summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt3
-rw-r--r--resources/qml/RoomDirectory.qml175
-rw-r--r--resources/qml/RoomList.qml12
-rw-r--r--resources/res.qrc6
-rw-r--r--src/RoomDirectoryModel.cpp186
-rw-r--r--src/RoomDirectoryModel.h88
-rw-r--r--src/timeline/TimelineViewManager.cpp7
7 files changed, 477 insertions, 0 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 049ed8a3..bcf31b41 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -360,6 +360,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
@@ -564,6 +565,8 @@ qt5_wrap_cpp(MOC_HEADERS
 	src/TrayIcon.h
 	src/UserSettingsPage.h
 	src/UsersModel.h
+	src/RoomsModel.h
+	src/RoomDirectoryModel.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..f31df64d
--- /dev/null
+++ b/resources/qml/RoomDirectory.qml
@@ -0,0 +1,175 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors

+//

+// SPDX-License-Identifier: GPL-3.0-or-later

+

+import QtQuick 2.9

+import QtQuick.Controls 2.3

+import QtQuick.Layouts 1.3

+import im.nheko 1.0

+import im.nheko.RoomDirectoryModel 1.0

+

+ApplicationWindow {

+    id: roomDirectoryWindow

+    visible: true

+

+    x: MainWindow.x + (MainWindow.width / 2) - (width / 2)

+    y: MainWindow.y + (MainWindow.height / 2) - (height / 2)

+    minimumWidth: 650

+    minimumHeight: 420

+    palette: Nheko.colors

+    color: Nheko.colors.window

+    modality: Qt.WindowModal

+    flags: Qt.Dialog | Qt.WindowCloseButtonHint

+    title: qsTr("Explore Public Rooms")

+

+    Shortcut {

+        sequence: StandardKey.Cancel

+        onActivated: roomDirectoryWindow.close()

+    }

+

+    header: RowLayout {

+        id: searchBarLayout

+        spacing: Nheko.paddingMedium

+        width: parent.width      

+

+        implicitHeight: roomSearch.height

+

+        MatrixTextField {

+            id: roomSearch

+

+            Layout.fillWidth: true

+

+            font.pixelSize: fontMetrics.font.pixelSize

+            padding: Math.ceil(1.5 * Nheko.paddingSmall)

+            color: Nheko.colors.text

+            placeholderText: qsTr("Search for public rooms")

+            onTextChanged: searchTimer.restart()

+        }

+

+        Timer {

+            id: searchTimer

+

+            interval: 350

+            onTriggered: roomDirView.model.setSearchTerm(roomSearch.text)

+        }

+    }

+

+    ListView {

+        id: roomDirView

+        anchors.fill: parent

+        height: parent.height - searchBarLayout.height

+        model: RoomDirectoryModel {

+            id: roomDir

+        }

+        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: Math.ceil(fontMetrics.lineSpacing * 4)

+

+            color: background

+            

+            height: avatarSize + 2.5 * Nheko.paddingMedium

+            width: ListView.view.width

+

+            RowLayout {

+

+                spacing: Nheko.paddingMedium

+                anchors.fill: parent

+                anchors.margins: Nheko.paddingMedium

+                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

+                    Layout.fillWidth: true

+                    width: parent.width - avatar.width

+                    Layout.preferredWidth: parent.width - avatar.width

+                    Layout.preferredHeight: roomNameRow.height + roomDescriptionRow.height

+                    spacing: Nheko.paddingSmall

+

+                    RowLayout {

+                        id: roomNameRow

+                        Layout.fillWidth: true

+                        spacing: 0

+

+                        ElidedLabel {

+                            Layout.alignment: Qt.AlignBottom

+                            color: roomDirDelegate.importantText

+                            elideWidth: textContent.width * 0.5 - Nheko.paddingMedium

+                            font.pixelSize: fontMetrics.font.pixelSize * 1.1

+                            fullText: model.name

+                        }

+                    }

+

+                    RowLayout {

+                        id: roomDescriptionRow

+                        Layout.fillWidth: true

+                        Layout.preferredWidth: parent.width

+                        spacing: Nheko.paddingSmall

+                        Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft

+			Layout.preferredHeight: Math.ceil(fontMetrics.lineSpacing * 4)

+

+                        Label {

+                            id: roomTopic

+                            color: roomDirDelegate.unimportantText

+                            font.weight: Font.Thin

+			    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.fillWidth: false

+			  Layout.margins: Nheko.paddingSmall

+                          width: roomCount.width

+

+                        Label {

+                            id: roomCount

+                            color: roomDirDelegate.unimportantText

+			    anchors.centerIn: parent

+                            Layout.fillWidth: false

+                            font.weight: Font.Thin

+                            font.pixelSize: fontMetrics.font.pixelSize

+                            text: model.numMembers.toString()

+                        }

+			}

+

+			Item {

+				id: buttonRectangle

+				Layout.fillWidth: false

+				Layout.margins: Nheko.paddingSmall	

+                        	width: joinRoomButton.width

+				Button {

+                            		id: joinRoomButton

+			    		visible: roomDir.canJoinRoom(model.roomid)

+					anchors.centerIn: parent 

+                            		width: Math.ceil(0.1 * roomDirectoryWindow.width)

+					text: "Join"

+                            		onClicked: roomDir.joinRoom(model.index)

+                        	}		

+			}

+                    }

+                }

+            }

+        } 

+    }

+}

diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index 98532606..e8aacf75 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
 
@@ -557,6 +564,11 @@ Page {
                     ToolTip.visible: hovered
                     ToolTip.text: qsTr("Room directory")
                     Layout.margins: Nheko.paddingMedium
+			onClicked: {
+                        console.debug("Roomdir clicked");
+                        var win = roomDirectoryComponent.createObject(timelineRoot);
+                        win.show();
+                    } 
                 }
 
                 ImageButton {
diff --git a/resources/res.qrc b/resources/res.qrc
index f50265ca..3e417d4c 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -142,6 +142,12 @@
         <file>qml/emoji/EmojiPicker.qml</file>
         <file>qml/emoji/StickerPicker.qml</file>
         <file>qml/UserProfile.qml</file>
+        <file>qml/RoomDirectory.qml</file>
+	      <file>qml/delegates/MessageDelegate.qml</file>
+        <file>qml/delegates/TextMessage.qml</file>
+        <file>qml/delegates/NoticeMessage.qml</file>
+        <file>qml/delegates/ImageMessage.qml</file>
+        <file>qml/delegates/PlayableMediaMessage.qml</file>
         <file>qml/delegates/MessageDelegate.qml</file>
         <file>qml/delegates/Encrypted.qml</file>
         <file>qml/delegates/FileMessage.qml</file>
diff --git a/src/RoomDirectoryModel.cpp b/src/RoomDirectoryModel.cpp
new file mode 100644
index 00000000..06bd9d8a
--- /dev/null
+++ b/src/RoomDirectoryModel.cpp
@@ -0,0 +1,186 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors

+//

+// SPDX-License-Identifier: GPL-3.0-or-later

+

+#include "RoomDirectoryModel.h"

+#include "Cache.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();

+}

+

+bool

+RoomDirectoryModel::canJoinRoom(const QByteArray &room)

+{

+        const auto &cache = cache::roomInfo();

+        const QString room_id(room);

+        const bool validRoom = !room_id.isNull() && !room_id.isEmpty();

+        return validRoom && !cache.contains(room_id);

+}

+

+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...");

+

+        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_ = next_batch;

+}

diff --git a/src/RoomDirectoryModel.h b/src/RoomDirectoryModel.h
new file mode 100644
index 00000000..c7089a1e
--- /dev/null
+++ b/src/RoomDirectoryModel.h
@@ -0,0 +1,88 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors

+//

+// SPDX-License-Identifier: GPL-3.0-or-later

+

+#pragma once

+

+#include <QAbstractListModel>

+#include <QHash>

+#include <QString>

+#include <string>

+#include <vector>

+

+#include "MatrixClient.h"

+#include <mtx/responses/public_rooms.hpp>

+#include <mtxclient/http/errors.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 bool canJoinRoom(const QByteArray &room);

+        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();

+};

diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index b23ed278..da68d503 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<DeviceInfo>)
+Q_DECLARE_METATYPE(std::vector<mtx::responses::PublicRoomsChunk>)
 
 namespace msgs = mtx::events::msg;
 
@@ -151,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,
@@ -282,6 +286,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);