Merge pull request #659 from kamathmanu/nhekoRoomDirectory

Nheko room directory
This commit is contained in:
DeepBlueV7.X 2021-08-17 11:56:47 +00:00 committed by GitHub
commit b01496f9b4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 515 additions and 0 deletions

View file

@ -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

View file

@ -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)
}
}
}

View file

@ -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 {

View file

@ -142,6 +142,7 @@
<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/Encrypted.qml</file>
<file>qml/delegates/FileMessage.qml</file>

194
src/RoomDirectoryModel.cpp Normal file
View file

@ -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 <algorithm>
RoomDirectoryModel::RoomDirectoryModel(QObject *parent, const std::string &s)
: QAbstractListModel(parent)
, server_(s)
{
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;
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<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) { return alias.substr(alias.find(":") + 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 &)
{
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<mtx::responses::PublicRoomsChunk> 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<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;
reachedEndOfPagination_ = true;
emit reachedEndOfPaginationChanged();
}
prevBatch_ = next_batch;
nhlog::ui()->debug("Finished loading rooms");
}

96
src/RoomDirectoryModel.h Normal file
View file

@ -0,0 +1,96 @@
// 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
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<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());
}
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<mtx::responses::PublicRoomsChunk> 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<mtx::responses::PublicRoomsChunk> 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<mtx::responses::PublicRoomsChunk> publicRoomsData_;
std::vector<std::string> getViasForRoom(const std::vector<std::string> &room);
void resetDisplayedData();
};

View file

@ -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,8 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
"EmojiCategory",
"Error: Only enums");
qmlRegisterType<RoomDirectoryModel>("im.nheko", 1, 0, "RoomDirectoryModel");
#ifdef USE_QUICK_VIEW
view = new QQuickView(parent);
container = QWidget::createWindowContainer(view, parent);