diff options
-rw-r--r-- | CMakeLists.txt | 3 | ||||
-rw-r--r-- | resources/qml/RoomDirectory.qml | 175 | ||||
-rw-r--r-- | resources/qml/RoomList.qml | 12 | ||||
-rw-r--r-- | resources/res.qrc | 6 | ||||
-rw-r--r-- | src/RoomDirectoryModel.cpp | 186 | ||||
-rw-r--r-- | src/RoomDirectoryModel.h | 88 | ||||
-rw-r--r-- | src/timeline/TimelineViewManager.cpp | 7 |
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); |