Port RoomList to TreeView

Use a tree model for the room list

closes network/neochat#156

BUG: 456643
This commit is contained in:
Tobias Fella
2024-02-19 20:09:43 +00:00
committed by James Graham
parent dae23ccd4b
commit fc6ea0b779
23 changed files with 1052 additions and 550 deletions

View File

@@ -56,6 +56,8 @@ add_library(neochat STATIC
notificationsmanager.h
models/sortfilterroomlistmodel.cpp
models/sortfilterroomlistmodel.h
models/roomtreemodel.cpp
models/roomtreemodel.h
chatdocumenthandler.cpp
chatdocumenthandler.h
models/devicesmodel.cpp
@@ -156,6 +158,9 @@ add_library(neochat STATIC
enums/messagecomponenttype.h
models/messagecontentmodel.cpp
models/messagecontentmodel.h
enums/neochatroomtype.h
models/sortfilterroomtreemodel.cpp
models/sortfilterroomtreemodel.h
)
qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
@@ -315,6 +320,7 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/LinkPreviewComponent.qml
qml/LoadComponent.qml
qml/RecommendedSpaceDialog.qml
qml/RoomTreeSection.qml
RESOURCES
qml/confetti.png
qml/glowdot.png

View File

@@ -0,0 +1,97 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <QObject>
#include "neochatroom.h"
#include <Quotient/quotient_common.h>
#include <KLocalizedString>
class NeoChatRoomType : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
/**
* @brief Defines the room list categories a room can be assigned.
*/
enum Types {
Search = 0, /**< So we can show a search delegate if needed, e.g. collapsed mode. */
Invited, /**< The user has been invited to the room. */
Favorite, /**< The room is set as a favourite. */
Direct, /**< The room is a direct chat. */
Normal, /**< The default category for a joined room. */
Deprioritized, /**< The room is set as low priority. */
Space, /**< The room is a space. */
AddDirect, /**< So we can show the add friend delegate. */
};
Q_ENUM(Types);
static NeoChatRoomType::Types typeForRoom(const NeoChatRoom *room)
{
if (room->isSpace()) {
return NeoChatRoomType::Space;
}
if (room->joinState() == Quotient::JoinState::Invite) {
return NeoChatRoomType::Invited;
}
if (room->isFavourite()) {
return NeoChatRoomType::Favorite;
}
if (room->isLowPriority()) {
return NeoChatRoomType::Deprioritized;
}
if (room->isDirectChat()) {
return NeoChatRoomType::Direct;
}
return NeoChatRoomType::Normal;
}
static QString typeName(int category)
{
switch (category) {
case NeoChatRoomType::Invited:
return i18n("Invited");
case NeoChatRoomType::Favorite:
return i18n("Favorite");
case NeoChatRoomType::Direct:
return i18n("Friends");
case NeoChatRoomType::Normal:
return i18n("Normal");
case NeoChatRoomType::Deprioritized:
return i18n("Low priority");
case NeoChatRoomType::Space:
return i18n("Spaces");
case NeoChatRoomType::Search:
return i18n("Search");
default:
return {};
}
}
static QString typeIconName(int category)
{
switch (category) {
case NeoChatRoomType::Invited:
return QStringLiteral("user-invisible");
case NeoChatRoomType::Favorite:
return QStringLiteral("favorite");
case NeoChatRoomType::Direct:
return QStringLiteral("dialog-messages");
case NeoChatRoomType::Normal:
return QStringLiteral("group");
case NeoChatRoomType::Deprioritized:
return QStringLiteral("object-order-lower");
case NeoChatRoomType::Space:
return QStringLiteral("group");
case NeoChatRoomType::Search:
return QStringLiteral("search");
default:
return QStringLiteral("tools-report-bug");
}
}
};

View File

@@ -28,11 +28,6 @@ Q_DECLARE_METATYPE(Quotient::JoinState)
RoomListModel::RoomListModel(QObject *parent)
: QAbstractListModel(parent)
{
const auto collapsedSections = NeoChatConfig::collapsedSections();
for (auto collapsedSection : collapsedSections) {
m_categoryVisibility[collapsedSection] = false;
}
connect(this, &RoomListModel::highlightCountChanged, this, [this]() {
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
#ifndef Q_OS_ANDROID
@@ -298,7 +293,7 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
return room->topic();
}
if (role == CategoryRole) {
return category(room);
return NeoChatRoomType::typeForRoom(room);
}
if (role == NotificationCountRole) {
return room->notificationCount();
@@ -318,9 +313,6 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
if (role == CurrentRoomRole) {
return QVariant::fromValue(room);
}
if (role == CategoryVisibleRole) {
return m_categoryVisibility.value(data(index, CategoryRole).toInt(), true);
}
if (role == SubtitleTextRole) {
if (room->lastEvent() == nullptr || room->lastEventIsSpoiler()) {
return QString();
@@ -374,7 +366,6 @@ QHash<int, QByteArray> RoomListModel::roleNames() const
roles[LastActiveTimeRole] = "lastActiveTime";
roles[JoinStateRole] = "joinState";
roles[CurrentRoomRole] = "currentRoom";
roles[CategoryVisibleRole] = "categoryVisible";
roles[SubtitleTextRole] = "subtitleText";
roles[IsSpaceRole] = "isSpace";
roles[RoomIdRole] = "roomId";
@@ -383,87 +374,6 @@ QHash<int, QByteArray> RoomListModel::roleNames() const
return roles;
}
NeoChatRoomType::Types RoomListModel::category(NeoChatRoom *room)
{
if (room->isSpace()) {
return NeoChatRoomType::Space;
}
if (room->joinState() == JoinState::Invite) {
return NeoChatRoomType::Invited;
}
if (room->isFavourite()) {
return NeoChatRoomType::Favorite;
}
if (room->isLowPriority()) {
return NeoChatRoomType::Deprioritized;
}
if (room->isDirectChat()) {
return NeoChatRoomType::Direct;
}
return NeoChatRoomType::Normal;
}
QString RoomListModel::categoryName(int category)
{
switch (category) {
case NeoChatRoomType::Invited:
return i18n("Invited");
case NeoChatRoomType::Favorite:
return i18n("Favorite");
case NeoChatRoomType::Direct:
return i18n("Friends");
case NeoChatRoomType::Normal:
return i18n("Normal");
case NeoChatRoomType::Deprioritized:
return i18n("Low priority");
case NeoChatRoomType::Space:
return i18n("Spaces");
default:
return {};
}
}
QString RoomListModel::categoryIconName(int category)
{
switch (category) {
case NeoChatRoomType::Invited:
return QStringLiteral("user-invisible");
case NeoChatRoomType::Favorite:
return QStringLiteral("favorite");
case NeoChatRoomType::Direct:
return QStringLiteral("dialog-messages");
case NeoChatRoomType::Normal:
return QStringLiteral("group");
case NeoChatRoomType::Deprioritized:
return QStringLiteral("object-order-lower");
case NeoChatRoomType::Space:
return QStringLiteral("group");
default:
return QStringLiteral("tools-report-bug");
}
}
void RoomListModel::setCategoryVisible(int category, bool visible)
{
beginResetModel();
auto collapsedSections = NeoChatConfig::collapsedSections();
if (visible) {
collapsedSections.removeAll(category);
} else {
collapsedSections.push_back(category);
}
NeoChatConfig::setCollapsedSections(collapsedSections);
NeoChatConfig::self()->save();
m_categoryVisibility[category] = visible;
endResetModel();
}
bool RoomListModel::categoryVisible(int category) const
{
return m_categoryVisibility.value(category, true);
}
NeoChatRoom *RoomListModel::roomByAliasOrId(const QString &aliasOrId)
{
for (const auto &room : std::as_const(m_rooms)) {

View File

@@ -6,6 +6,8 @@
#include <QAbstractListModel>
#include <QQmlEngine>
#include "enums/neochatroomtype.h"
class NeoChatRoom;
namespace Quotient
@@ -14,27 +16,6 @@ class Connection;
class Room;
}
class NeoChatRoomType : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
/**
* @brief Defines the room list categories a room can be assigned.
*/
enum Types {
Invited = 1, /**< The user has been invited to the room. */
Favorite, /**< The room is set as a favourite. */
Direct, /**< The room is a direct chat. */
Normal, /**< The default category for a joined room. */
Deprioritized, /**< The room is set as low priority. */
Space, /**< The room is a space. */
};
Q_ENUM(Types)
};
/**
* @class RoomListModel
*
@@ -70,7 +51,6 @@ public:
LastActiveTimeRole, /**< The timestamp of the last event sent in the room. */
JoinStateRole, /**< The local user's join state in the room. */
CurrentRoomRole, /**< The room object for the room. */
CategoryVisibleRole, /**< If the room's category is visible. */
SubtitleTextRole, /**< The text to show as the room subtitle. */
AvatarImageRole, /**< The room avatar as an image. */
RoomIdRole, /**< The room matrix ID. */
@@ -116,35 +96,6 @@ public:
*/
Q_INVOKABLE [[nodiscard]] NeoChatRoom *roomAt(int row) const;
/**
* @brief The category for the given room.
*/
static NeoChatRoomType::Types category(NeoChatRoom *room);
/**
* @brief Return a string to represent the given room category.
*/
Q_INVOKABLE [[nodiscard]] static QString categoryName(int category);
/**
* @brief Return a string with the name of the given room category icon.
*/
Q_INVOKABLE [[nodiscard]] static QString categoryIconName(int category);
/**
* @brief Set whether a given category should be visible or not.
*
* @param category the NeoChatRoomType::Types value for the category (it's an
* int due to the pain of Q_INVOKABLES and cpp enums).
* @param visible true if the category should be visible, false if not.
*/
Q_INVOKABLE void setCategoryVisible(int category, bool visible);
/**
* @brief Return whether a room category is set to be visible.
*/
Q_INVOKABLE [[nodiscard]] bool categoryVisible(int category) const;
/**
* @brief Return the model row for the given room.
*/
@@ -170,8 +121,6 @@ private:
Quotient::Connection *m_connection = nullptr;
QList<NeoChatRoom *> m_rooms;
QMap<int, bool> m_categoryVisibility;
int m_notificationCount = 0;
int m_highlightCount = 0;
QString m_activeSpaceId;

View File

@@ -0,0 +1,323 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "roomtreemodel.h"
#include <Quotient/connection.h>
#include <Quotient/room.h>
#include "eventhandler.h"
#include "neochatconnection.h"
#include "neochatroomtype.h"
#include "spacehierarchycache.h"
using namespace Quotient;
RoomTreeModel::RoomTreeModel(QObject *parent)
: QAbstractItemModel(parent)
{
initializeCategories();
}
void RoomTreeModel::initializeCategories()
{
for (const auto &key : m_rooms.keys()) {
for (const auto &room : m_rooms[key]) {
room->disconnect(this);
}
}
m_rooms.clear();
for (int i = 0; i < 8; i++) {
m_rooms[NeoChatRoomType::Types(i)] = {};
}
}
void RoomTreeModel::setConnection(NeoChatConnection *connection)
{
if (m_connection == connection) {
return;
}
disconnect(m_connection.get(), nullptr, this, nullptr);
m_connection = connection;
beginResetModel();
initializeCategories();
endResetModel();
connect(connection, &Connection::newRoom, this, &RoomTreeModel::newRoom);
connect(connection, &Connection::leftRoom, this, &RoomTreeModel::leftRoom);
for (const auto &room : m_connection->allRooms()) {
newRoom(dynamic_cast<NeoChatRoom *>(room));
}
Q_EMIT connectionChanged();
}
void RoomTreeModel::newRoom(Room *r)
{
const auto room = dynamic_cast<NeoChatRoom *>(r);
const auto type = NeoChatRoomType::typeForRoom(room);
beginInsertRows(index(type, 0), m_rooms[type].size(), m_rooms[type].size());
m_rooms[type].append(room);
connectRoomSignals(room);
endInsertRows();
}
void RoomTreeModel::leftRoom(Room *r)
{
const auto room = dynamic_cast<NeoChatRoom *>(r);
const auto type = NeoChatRoomType::typeForRoom(room);
auto row = m_rooms[type].indexOf(room);
if (row == -1) {
return;
}
beginRemoveRows(index(type, 0), row, row);
m_rooms[type][row]->disconnect(this);
m_rooms[type].removeAt(row);
endRemoveRows();
}
void RoomTreeModel::moveRoom(Quotient::Room *room)
{
// We can't assume the type as it has changed so currently the return of
// NeoChatRoomType::typeForRoom doesn't match it's current location. So find the room.
NeoChatRoomType::Types oldType;
int oldRow = -1;
for (const auto &key : m_rooms.keys()) {
if (m_rooms[key].contains(room)) {
oldType = key;
oldRow = m_rooms[key].indexOf(room);
}
}
if (oldRow == -1) {
return;
}
const auto newType = NeoChatRoomType::typeForRoom(dynamic_cast<NeoChatRoom *>(room));
if (newType == oldType) {
return;
}
const auto oldParent = index(oldType, 0, {});
const auto newParent = index(newType, 0, {});
// HACK: We're doing this as a remove then insert because moving doesn't work
// properly with DelegateChooser for whatever reason.
beginRemoveRows(oldParent, oldRow, oldRow);
m_rooms[oldType].removeAt(oldRow);
endRemoveRows();
beginInsertRows(newParent, m_rooms[newType].size(), m_rooms[newType].size());
m_rooms[newType].append(dynamic_cast<NeoChatRoom *>(room));
endInsertRows();
}
void RoomTreeModel::connectRoomSignals(NeoChatRoom *room)
{
connect(room, &Room::displaynameChanged, this, [this, room] {
refreshRoomRoles(room, {DisplayNameRole});
});
connect(room, &Room::unreadStatsChanged, this, [this, room] {
refreshRoomRoles(room, {NotificationCountRole, HighlightCountRole});
});
connect(room, &Room::avatarChanged, this, [this, room] {
refreshRoomRoles(room, {AvatarRole});
});
connect(room, &Room::tagsChanged, this, [this, room] {
moveRoom(room);
});
connect(room, &Room::joinStateChanged, this, [this, room] {
refreshRoomRoles(room);
});
connect(room, &Room::addedMessages, this, [this, room] {
refreshRoomRoles(room, {SubtitleTextRole, LastActiveTimeRole});
});
connect(room, &Room::pendingEventMerged, this, [this, room] {
refreshRoomRoles(room, {SubtitleTextRole});
});
}
void RoomTreeModel::refreshRoomRoles(NeoChatRoom *room, const QList<int> &roles)
{
const auto roomType = NeoChatRoomType::typeForRoom(room);
const auto it = std::find(m_rooms[roomType].begin(), m_rooms[roomType].end(), room);
if (it == m_rooms[roomType].end()) {
qCritical() << "Room" << room->id() << "not found in the room list";
return;
}
const auto parentIndex = index(roomType, 0, {});
const auto idx = index(it - m_rooms[roomType].begin(), 0, parentIndex);
Q_EMIT dataChanged(idx, idx, roles);
}
NeoChatConnection *RoomTreeModel::connection() const
{
return m_connection;
}
int RoomTreeModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return 1;
}
int RoomTreeModel::rowCount(const QModelIndex &parent) const
{
if (!parent.isValid()) {
return m_rooms.keys().size();
}
if (!parent.parent().isValid()) {
return m_rooms.values()[parent.row()].size();
}
return 0;
}
QModelIndex RoomTreeModel::parent(const QModelIndex &index) const
{
if (!index.internalPointer()) {
return {};
}
return this->index(NeoChatRoomType::typeForRoom(static_cast<NeoChatRoom *>(index.internalPointer())), 0, QModelIndex());
}
QModelIndex RoomTreeModel::index(int row, int column, const QModelIndex &parent) const
{
if (!parent.isValid()) {
return createIndex(row, column, nullptr);
}
if (row >= rowCount(parent)) {
return {};
}
return createIndex(row, column, m_rooms[NeoChatRoomType::Types(parent.row())][row]);
}
QHash<int, QByteArray> RoomTreeModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[DisplayNameRole] = "displayName";
roles[AvatarRole] = "avatar";
roles[CanonicalAliasRole] = "canonicalAlias";
roles[TopicRole] = "topic";
roles[CategoryRole] = "category";
roles[NotificationCountRole] = "notificationCount";
roles[HighlightCountRole] = "highlightCount";
roles[LastActiveTimeRole] = "lastActiveTime";
roles[JoinStateRole] = "joinState";
roles[CurrentRoomRole] = "currentRoom";
roles[SubtitleTextRole] = "subtitleText";
roles[IsSpaceRole] = "isSpace";
roles[RoomIdRole] = "roomId";
roles[IsChildSpaceRole] = "isChildSpace";
roles[IsDirectChat] = "isDirectChat";
roles[DelegateTypeRole] = "delegateType";
roles[IconRole] = "icon";
return roles;
}
// TODO room type changes
QVariant RoomTreeModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return QVariant();
}
if (!index.parent().isValid()) {
if (role == DisplayNameRole) {
return NeoChatRoomType::typeName(index.row());
}
if (role == DelegateTypeRole) {
if (index.row() == NeoChatRoomType::Search) {
return QStringLiteral("search");
}
if (index.row() == NeoChatRoomType::AddDirect) {
return QStringLiteral("addDirect");
}
return QStringLiteral("section");
}
if (role == IconRole) {
return NeoChatRoomType::typeIconName(index.row());
}
if (role == CategoryRole) {
return index.row();
}
return {};
}
const auto room = m_rooms.values()[index.parent().row()][index.row()].get();
Q_ASSERT(room);
if (role == DisplayNameRole) {
return room->displayName();
}
if (role == AvatarRole) {
return room->avatarMediaId();
}
if (role == CanonicalAliasRole) {
return room->canonicalAlias();
}
if (role == TopicRole) {
return room->topic();
}
if (role == CategoryRole) {
return NeoChatRoomType::typeForRoom(room);
}
if (role == NotificationCountRole) {
return room->notificationCount();
}
if (role == HighlightCountRole) {
return room->highlightCount();
}
if (role == LastActiveTimeRole) {
return room->lastActiveTime();
}
if (role == JoinStateRole) {
if (!room->successorId().isEmpty()) {
return QStringLiteral("upgraded");
}
return QVariant::fromValue(room->joinState());
}
if (role == CurrentRoomRole) {
return QVariant::fromValue(room);
}
if (role == SubtitleTextRole) {
if (room->lastEvent() == nullptr || room->lastEventIsSpoiler()) {
return QString();
}
EventHandler eventHandler(room, room->lastEvent());
return eventHandler.subtitleText();
}
if (role == AvatarImageRole) {
return room->avatar(128);
}
if (role == RoomIdRole) {
return room->id();
}
if (role == IsSpaceRole) {
return room->isSpace();
}
if (role == IsChildSpaceRole) {
return SpaceHierarchyCache::instance().isChild(room->id());
}
if (role == ReplacementIdRole) {
return room->successorId();
}
if (role == IsDirectChat) {
return room->isDirectChat();
}
if (role == DelegateTypeRole) {
return QStringLiteral("normal");
}
return {};
}
QModelIndex RoomTreeModel::indexForRoom(NeoChatRoom *room) const
{
if (room == nullptr) {
return {};
}
const auto type = NeoChatRoomType::typeForRoom(room);
auto row = m_rooms[type].indexOf(room);
if (row >= 0) {
return index(row, 0, index(type, 0));
}
return {};
}
#include "moc_roomtreemodel.cpp"

View File

@@ -0,0 +1,94 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <QAbstractItemModel>
#include <QPointer>
#include "enums/neochatroomtype.h"
namespace Quotient
{
class Room;
}
class NeoChatConnection;
class NeoChatRoom;
class RoomTreeModel : public QAbstractItemModel
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
public:
/**
* @brief Defines the model roles.
*/
enum EventRoles {
DisplayNameRole = Qt::DisplayRole, /**< The display name of the room. */
AvatarRole, /**< The source URL for the room's avatar. */
CanonicalAliasRole, /**< The room canonical alias. */
TopicRole, /**< The room topic. */
CategoryRole, /**< The room category, e.g favourite. */
NotificationCountRole, /**< The number of notifications in the room. */
HighlightCountRole, /**< The number of highlighted messages in the room. */
LastActiveTimeRole, /**< The timestamp of the last event sent in the room. */
JoinStateRole, /**< The local user's join state in the room. */
CurrentRoomRole, /**< The room object for the room. */
SubtitleTextRole, /**< The text to show as the room subtitle. */
AvatarImageRole, /**< The room avatar as an image. */
RoomIdRole, /**< The room matrix ID. */
IsSpaceRole, /**< Whether the room is a space. */
IsChildSpaceRole, /**< Whether this space is a child of a different space. */
ReplacementIdRole, /**< The room id of the room replacing this one, if any. */
IsDirectChat, /**< Whether this room is a direct chat. */
DelegateTypeRole,
IconRole,
};
Q_ENUM(EventRoles)
explicit RoomTreeModel(QObject *parent = nullptr);
void setConnection(NeoChatConnection *connection);
NeoChatConnection *connection() const;
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa EventRoles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
QModelIndex parent(const QModelIndex &index) const override;
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
Q_INVOKABLE QModelIndex indexForRoom(NeoChatRoom *room) const;
Q_SIGNALS:
void connectionChanged();
private:
QPointer<NeoChatConnection> m_connection = nullptr;
QMap<NeoChatRoomType::Types, QList<QPointer<NeoChatRoom>>> m_rooms;
void initializeCategories();
void connectRoomSignals(NeoChatRoom *room);
void newRoom(Quotient::Room *room);
void leftRoom(Quotient::Room *room);
void moveRoom(Quotient::Room *room);
void refreshRoomRoles(NeoChatRoom *room, const QList<int> &roles = {});
};

View File

@@ -3,9 +3,7 @@
#include "sortfilterroomlistmodel.h"
#include "neochatconnection.h"
#include "roomlistmodel.h"
#include "spacehierarchycache.h"
SortFilterRoomListModel::SortFilterRoomListModel(QObject *parent)
: QSortFilterProxyModel(parent)
@@ -21,53 +19,6 @@ SortFilterRoomListModel::SortFilterRoomListModel(QObject *parent)
});
}
void SortFilterRoomListModel::setRoomSortOrder(SortFilterRoomListModel::RoomSortOrder sortOrder)
{
m_sortOrder = sortOrder;
Q_EMIT roomSortOrderChanged();
if (sortOrder == SortFilterRoomListModel::Alphabetical) {
setSortRole(RoomListModel::DisplayNameRole);
} else if (sortOrder == SortFilterRoomListModel::LastActivity) {
setSortRole(RoomListModel::LastActiveTimeRole);
}
invalidate();
}
SortFilterRoomListModel::RoomSortOrder SortFilterRoomListModel::roomSortOrder() const
{
return m_sortOrder;
}
bool SortFilterRoomListModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
{
if (m_sortOrder == SortFilterRoomListModel::LastActivity) {
// display favorite rooms always on top
const auto categoryLeft = static_cast<NeoChatRoomType::Types>(sourceModel()->data(source_left, RoomListModel::CategoryRole).toInt());
const auto categoryRight = static_cast<NeoChatRoomType::Types>(sourceModel()->data(source_right, RoomListModel::CategoryRole).toInt());
if (categoryLeft == NeoChatRoomType::Types::Favorite && categoryRight == NeoChatRoomType::Types::Favorite) {
return sourceModel()->data(source_left, RoomListModel::LastActiveTimeRole).toDateTime()
> sourceModel()->data(source_right, RoomListModel::LastActiveTimeRole).toDateTime();
}
if (categoryLeft == NeoChatRoomType::Types::Favorite) {
return true;
} else if (categoryRight == NeoChatRoomType::Types::Favorite) {
return false;
}
return sourceModel()->data(source_left, RoomListModel::LastActiveTimeRole).toDateTime()
> sourceModel()->data(source_right, RoomListModel::LastActiveTimeRole).toDateTime();
}
if (m_sortOrder != SortFilterRoomListModel::Categories) {
return QSortFilterProxyModel::lessThan(source_left, source_right);
}
if (sourceModel()->data(source_left, RoomListModel::CategoryRole) != sourceModel()->data(source_right, RoomListModel::CategoryRole)) {
return sourceModel()->data(source_left, RoomListModel::CategoryRole).toInt() < sourceModel()->data(source_right, RoomListModel::CategoryRole).toInt();
}
return sourceModel()->data(source_left, RoomListModel::LastActiveTimeRole).toDateTime()
> sourceModel()->data(source_right, RoomListModel::LastActiveTimeRole).toDateTime();
}
void SortFilterRoomListModel::setFilterText(const QString &text)
{
m_filterText = text;
@@ -81,69 +32,15 @@ QString SortFilterRoomListModel::filterText() const
bool SortFilterRoomListModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
Q_UNUSED(source_parent);
QModelIndex index = sourceModel()->index(source_row, 0, source_parent);
bool acceptRoom =
sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::DisplayNameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
&& sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IsSpaceRole).toBool() == false;
bool isDirectChat = sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IsDirectChat).toBool();
// In `show direct chats` mode we only care about whether or not it's a direct chat or if the filter string matches.'
if (m_mode == DirectChats) {
return isDirectChat && acceptRoom;
}
// When not in `show direct chats` mode, filter them out.
if (isDirectChat && m_mode == Rooms) {
if (sourceModel()->data(index, RoomListModel::JoinStateRole).toString() == QStringLiteral("upgraded")
&& dynamic_cast<RoomListModel *>(sourceModel())->connection()->room(sourceModel()->data(index, RoomListModel::ReplacementIdRole).toString())) {
return false;
}
if (sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::JoinStateRole).toString() == QStringLiteral("upgraded")
&& dynamic_cast<RoomListModel *>(sourceModel())
->connection()
->room(sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::ReplacementIdRole).toString())) {
return false;
}
if (m_activeSpaceId.isEmpty()) {
if (!SpaceHierarchyCache::instance().isChild(sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::RoomIdRole).toString())) {
return acceptRoom;
}
return false;
} else {
const auto &rooms = SpaceHierarchyCache::instance().getRoomListForSpace(m_activeSpaceId, false);
return std::find(rooms.begin(), rooms.end(), sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::RoomIdRole).toString())
!= rooms.end()
&& acceptRoom;
}
}
QString SortFilterRoomListModel::activeSpaceId() const
{
return m_activeSpaceId;
}
void SortFilterRoomListModel::setActiveSpaceId(const QString &spaceId)
{
m_activeSpaceId = spaceId;
Q_EMIT activeSpaceIdChanged();
invalidate();
}
SortFilterRoomListModel::Mode SortFilterRoomListModel::mode() const
{
return m_mode;
}
void SortFilterRoomListModel::setMode(SortFilterRoomListModel::Mode mode)
{
if (m_mode == mode) {
return;
}
m_mode = mode;
Q_EMIT modeChanged();
invalidate();
return sourceModel()->data(index, RoomListModel::DisplayNameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
&& sourceModel()->data(index, RoomListModel::IsSpaceRole).toBool() == false;
}
#include "moc_sortfilterroomlistmodel.cpp"

View File

@@ -30,65 +30,18 @@ class SortFilterRoomListModel : public QSortFilterProxyModel
Q_OBJECT
QML_ELEMENT
/**
* @brief The order by which the rooms will be sorted.
*
* @sa RoomSortOrder
*/
Q_PROPERTY(RoomSortOrder roomSortOrder READ roomSortOrder WRITE setRoomSortOrder NOTIFY roomSortOrderChanged)
/**
* @brief The text to use to filter room names.
*/
Q_PROPERTY(QString filterText READ filterText READ filterText WRITE setFilterText NOTIFY filterTextChanged)
/**
* @brief Set the ID of the space to show rooms for.
*/
Q_PROPERTY(QString activeSpaceId READ activeSpaceId WRITE setActiveSpaceId NOTIFY activeSpaceIdChanged)
/**
* @brief Whether only direct chats should be shown.
*/
Q_PROPERTY(Mode mode READ mode WRITE setMode NOTIFY modeChanged)
public:
enum RoomSortOrder {
Alphabetical,
LastActivity,
Categories,
};
Q_ENUM(RoomSortOrder)
enum Mode {
Rooms,
DirectChats,
All,
};
Q_ENUM(Mode)
explicit SortFilterRoomListModel(QObject *parent = nullptr);
void setRoomSortOrder(RoomSortOrder sortOrder);
[[nodiscard]] RoomSortOrder roomSortOrder() const;
void setFilterText(const QString &text);
[[nodiscard]] QString filterText() const;
QString activeSpaceId() const;
void setActiveSpaceId(const QString &spaceId);
Mode mode() const;
void setMode(Mode mode);
protected:
/**
* @brief Returns true if the value of source_left is less than source_right.
*
* @sa QSortFilterProxyModel::lessThan
*/
[[nodiscard]] bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override;
/**
* @brief Whether a row should be shown out or not.
*
@@ -97,14 +50,8 @@ protected:
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
Q_SIGNALS:
void roomSortOrderChanged();
void filterTextChanged();
void activeSpaceIdChanged();
void modeChanged();
private:
RoomSortOrder m_sortOrder = Categories;
Mode m_mode = All;
QString m_filterText;
QString m_activeSpaceId;
};

View File

@@ -0,0 +1,161 @@
// SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "sortfilterroomtreemodel.h"
#include "neochatconfig.h"
#include "neochatconnection.h"
#include "neochatroomtype.h"
#include "roomtreemodel.h"
#include "spacehierarchycache.h"
SortFilterRoomTreeModel::SortFilterRoomTreeModel(QObject *parent)
: QSortFilterProxyModel(parent)
{
setRecursiveFilteringEnabled(true);
sort(0);
invalidateFilter();
connect(this, &SortFilterRoomTreeModel::filterTextChanged, this, &SortFilterRoomTreeModel::invalidateFilter);
connect(this, &SortFilterRoomTreeModel::sourceModelChanged, this, [this]() {
sourceModel()->disconnect(this);
connect(sourceModel(), &QAbstractItemModel::rowsInserted, this, &SortFilterRoomTreeModel::invalidateFilter);
connect(sourceModel(), &QAbstractItemModel::rowsRemoved, this, &SortFilterRoomTreeModel::invalidateFilter);
});
connect(NeoChatConfig::self(), &NeoChatConfig::CollapsedChanged, this, &SortFilterRoomTreeModel::invalidateFilter);
}
void SortFilterRoomTreeModel::setRoomSortOrder(SortFilterRoomTreeModel::RoomSortOrder sortOrder)
{
m_sortOrder = sortOrder;
Q_EMIT roomSortOrderChanged();
if (sortOrder == SortFilterRoomTreeModel::Alphabetical) {
setSortRole(RoomTreeModel::DisplayNameRole);
} else if (sortOrder == SortFilterRoomTreeModel::LastActivity) {
setSortRole(RoomTreeModel::LastActiveTimeRole);
}
invalidate();
}
SortFilterRoomTreeModel::RoomSortOrder SortFilterRoomTreeModel::roomSortOrder() const
{
return m_sortOrder;
}
bool SortFilterRoomTreeModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
{
if (m_sortOrder == SortFilterRoomTreeModel::LastActivity) {
// display favorite rooms always on top
const auto categoryLeft = static_cast<NeoChatRoomType::Types>(sourceModel()->data(source_left, RoomTreeModel::CategoryRole).toInt());
const auto categoryRight = static_cast<NeoChatRoomType::Types>(sourceModel()->data(source_right, RoomTreeModel::CategoryRole).toInt());
if (categoryLeft == NeoChatRoomType::Types::Favorite && categoryRight == NeoChatRoomType::Types::Favorite) {
return sourceModel()->data(source_left, RoomTreeModel::LastActiveTimeRole).toDateTime()
> sourceModel()->data(source_right, RoomTreeModel::LastActiveTimeRole).toDateTime();
}
if (categoryLeft == NeoChatRoomType::Types::Favorite) {
return true;
} else if (categoryRight == NeoChatRoomType::Types::Favorite) {
return false;
}
return sourceModel()->data(source_left, RoomTreeModel::LastActiveTimeRole).toDateTime()
> sourceModel()->data(source_right, RoomTreeModel::LastActiveTimeRole).toDateTime();
}
if (m_sortOrder != SortFilterRoomTreeModel::Categories) {
return QSortFilterProxyModel::lessThan(source_left, source_right);
}
if (sourceModel()->data(source_left, RoomTreeModel::CategoryRole) != sourceModel()->data(source_right, RoomTreeModel::CategoryRole)) {
return sourceModel()->data(source_left, RoomTreeModel::CategoryRole).toInt() < sourceModel()->data(source_right, RoomTreeModel::CategoryRole).toInt();
}
return sourceModel()->data(source_left, RoomTreeModel::LastActiveTimeRole).toDateTime()
> sourceModel()->data(source_right, RoomTreeModel::LastActiveTimeRole).toDateTime();
}
void SortFilterRoomTreeModel::setFilterText(const QString &text)
{
m_filterText = text;
Q_EMIT filterTextChanged();
}
QString SortFilterRoomTreeModel::filterText() const
{
return m_filterText;
}
bool SortFilterRoomTreeModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
if (!source_parent.isValid()) {
if (sourceModel()->data(sourceModel()->index(source_row, 0), RoomTreeModel::CategoryRole).toInt() == NeoChatRoomType::Search
&& NeoChatConfig::collapsed()) {
return true;
}
if (sourceModel()->data(sourceModel()->index(source_row, 0), RoomTreeModel::CategoryRole).toInt() == NeoChatRoomType::AddDirect
&& m_mode == DirectChats) {
return true;
}
return false;
}
QModelIndex index = sourceModel()->index(source_row, 0, source_parent);
bool acceptRoom = sourceModel()->data(index, RoomTreeModel::DisplayNameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
&& sourceModel()->data(index, RoomTreeModel::IsSpaceRole).toBool() == false;
bool isDirectChat = sourceModel()->data(index, RoomTreeModel::IsDirectChat).toBool();
// In `show direct chats` mode we only care about whether or not it's a direct chat or if the filter string matches.'
if (m_mode == DirectChats) {
return isDirectChat && acceptRoom;
}
// When not in `show direct chats` mode, filter them out.
if (isDirectChat && m_mode == Rooms) {
return false;
}
if (sourceModel()->data(index, RoomTreeModel::JoinStateRole).toString() == QStringLiteral("upgraded")
&& dynamic_cast<RoomTreeModel *>(sourceModel())->connection()->room(sourceModel()->data(index, RoomTreeModel::ReplacementIdRole).toString())) {
return false;
}
if (m_activeSpaceId.isEmpty()) {
if (!SpaceHierarchyCache::instance().isChild(sourceModel()->data(index, RoomTreeModel::RoomIdRole).toString())) {
return acceptRoom;
}
return false;
} else {
const auto &rooms = SpaceHierarchyCache::instance().getRoomListForSpace(m_activeSpaceId, false);
return std::find(rooms.begin(), rooms.end(), sourceModel()->data(index, RoomTreeModel::RoomIdRole).toString()) != rooms.end() && acceptRoom;
}
}
QString SortFilterRoomTreeModel::activeSpaceId() const
{
return m_activeSpaceId;
}
void SortFilterRoomTreeModel::setActiveSpaceId(const QString &spaceId)
{
m_activeSpaceId = spaceId;
Q_EMIT activeSpaceIdChanged();
invalidate();
}
SortFilterRoomTreeModel::Mode SortFilterRoomTreeModel::mode() const
{
return m_mode;
}
void SortFilterRoomTreeModel::setMode(SortFilterRoomTreeModel::Mode mode)
{
if (m_mode == mode) {
return;
}
m_mode = mode;
Q_EMIT modeChanged();
invalidate();
}
#include "moc_sortfilterroomtreemodel.cpp"

View File

@@ -0,0 +1,111 @@
// SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QQmlEngine>
#include <QSortFilterProxyModel>
/**
* @class SortFilterRoomTreeModel
*
* This model sorts and filters the room list.
*
* There are numerous room sort orders available:
* - Categories - sort rooms by their NeoChatRoomType and then by last activty within
* each category.
* - LastActivity - sort rooms by the last active time in the room.
* - Alphabetical - sort the rooms alphabetically by room name.
*
* The model can be given a filter string that will only show rooms who's name includes
* the text.
*
* The model can also be given an active space ID and will only show rooms within
* that space.
*
* All space rooms and upgraded rooms will also be filtered out.
*/
class SortFilterRoomTreeModel : public QSortFilterProxyModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The order by which the rooms will be sorted.
*
* @sa RoomSortOrder
*/
Q_PROPERTY(RoomSortOrder roomSortOrder READ roomSortOrder WRITE setRoomSortOrder NOTIFY roomSortOrderChanged)
/**
* @brief The text to use to filter room names.
*/
Q_PROPERTY(QString filterText READ filterText READ filterText WRITE setFilterText NOTIFY filterTextChanged)
/**
* @brief Set the ID of the space to show rooms for.
*/
Q_PROPERTY(QString activeSpaceId READ activeSpaceId WRITE setActiveSpaceId NOTIFY activeSpaceIdChanged)
/**
* @brief Whether only direct chats should be shown.
*/
Q_PROPERTY(Mode mode READ mode WRITE setMode NOTIFY modeChanged)
public:
enum RoomSortOrder {
Alphabetical,
LastActivity,
Categories,
};
Q_ENUM(RoomSortOrder)
enum Mode {
Rooms,
DirectChats,
All,
};
Q_ENUM(Mode)
explicit SortFilterRoomTreeModel(QObject *parent = nullptr);
void setRoomSortOrder(RoomSortOrder sortOrder);
[[nodiscard]] RoomSortOrder roomSortOrder() const;
void setFilterText(const QString &text);
[[nodiscard]] QString filterText() const;
QString activeSpaceId() const;
void setActiveSpaceId(const QString &spaceId);
Mode mode() const;
void setMode(Mode mode);
protected:
/**
* @brief Returns true if the value of source_left is less than source_right.
*
* @sa QSortFilterProxyModel::lessThan
*/
[[nodiscard]] bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override;
/**
* @brief Whether a row should be shown out or not.
*
* @sa QSortFilterProxyModel::filterAcceptsRow
*/
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
Q_SIGNALS:
void roomSortOrderChanged();
void filterTextChanged();
void activeSpaceIdChanged();
void modeChanged();
private:
RoomSortOrder m_sortOrder = Categories;
Mode m_mode = All;
QString m_filterText;
QString m_activeSpaceId;
};

View File

@@ -106,7 +106,7 @@ void NeoChatConnection::connectSignals()
for (const auto room : allRooms()) {
connect(room, &NeoChatRoom::unreadStatsChanged, this, [this, room]() {
if (room != nullptr) {
auto category = RoomListModel::category(static_cast<NeoChatRoom *>(room));
auto category = NeoChatRoomType::typeForRoom(static_cast<NeoChatRoom *>(room));
if (!SpaceHierarchyCache::instance().isChild(room->id()) && (category == NeoChatRoomType::Normal || category == NeoChatRoomType::Favorite)
&& room->successorId().isEmpty()) {
Q_EMIT homeNotificationsChanged();
@@ -342,7 +342,7 @@ qsizetype NeoChatConnection::homeNotifications() const
QStringList added;
const auto &spaceHierarchyCache = SpaceHierarchyCache::instance();
for (const auto &room : allRooms()) {
auto category = RoomListModel::category(static_cast<NeoChatRoom *>(room));
auto category = NeoChatRoomType::typeForRoom(static_cast<NeoChatRoom *>(room));
if (!added.contains(room->id()) && room->joinState() == JoinState::Join && !room->isDirectChat() && !spaceHierarchyCache.isChild(room->id())
&& room->successorId().isEmpty()) {
switch (category) {

View File

@@ -1279,7 +1279,7 @@ void NeoChatRoom::removeParent(const QString &parentId)
}
}
bool NeoChatRoom::isSpace()
bool NeoChatRoom::isSpace() const
{
const auto creationEvent = this->creation();
if (!creationEvent) {

View File

@@ -531,7 +531,7 @@ public:
*/
Q_INVOKABLE void removeParent(const QString &parentId);
[[nodiscard]] bool isSpace();
[[nodiscard]] bool isSpace() const;
qsizetype childrenNotificationCount();

View File

@@ -30,8 +30,7 @@ Kirigami.ScrollablePage {
}
delegate: RoomDelegate {
id: roomDelegate
filterText: ""
onSelected: {
onClicked: {
root.chosen(roomDelegate.currentRoom.id);
}
connection: root.connection

View File

@@ -17,6 +17,8 @@ RowLayout {
property bool collapsed: false
required property NeoChatConnection connection
property alias roomSearchFieldFocussed: roomSearchField.activeFocus
property Kirigami.Action exploreAction: Kirigami.Action {
text: i18n("Explore rooms")
icon.name: "compass"
@@ -72,13 +74,14 @@ RowLayout {
signal textChanged(string newText)
Kirigami.SearchField {
id: roomSearchField
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
Layout.fillWidth: true
Layout.preferredWidth: root.desiredWidth ? root.desiredWidth - menuButton.width - root.spacing : -1
visible: !root.collapsed
onTextChanged: root.textChanged(text)
KeyNavigation.tab: listView
KeyNavigation.tab: treeView
}
QQC2.ToolButton {

View File

@@ -90,9 +90,8 @@ QQC2.Dialog {
}
delegate: RoomDelegate {
filterText: searchField.text
connection: root.connection
onSelected: root.close()
onClicked: root.close()
}
}
}

View File

@@ -22,35 +22,27 @@ Delegates.RoundedItemDelegate {
required property int highlightCount
required property NeoChatRoom currentRoom
required property NeoChatConnection connection
required property bool categoryVisible
required property string filterText
required property string avatar
required property string subtitleText
required property string displayName
property bool collapsed: false
readonly property bool hasNotifications: currentRoom.pushNotificationState === PushNotificationState.MentionKeyword || currentRoom.isLowPriority ? highlightCount > 0 : notificationCount > 0
signal selected
Accessible.name: root.displayName
Accessible.onPressAction: select()
Accessible.onPressAction: clicked()
onClicked: RoomManager.resolveResource(currentRoom.id);
onPressAndHold: createRoomListContextMenu()
Keys.onSpacePressed: select()
Keys.onEnterPressed: select()
Keys.onReturnPressed: select()
Keys.onSpacePressed: clicked()
Keys.onEnterPressed: clicked()
Keys.onReturnPressed: clicked()
TapHandler {
acceptedButtons: Qt.RightButton | Qt.LeftButton
onTapped: (eventPoint, button) => {
if (button === Qt.RightButton) {
root.createRoomListContextMenu();
} else {
select();
}
}
acceptedButtons: Qt.RightButton
onTapped: (eventPoint, button) => root.createRoomListContextMenu();
}
contentItem: RowLayout {
@@ -72,6 +64,7 @@ Delegates.RoundedItemDelegate {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
visible: !root.collapsed
QQC2.Label {
id: label
@@ -105,7 +98,7 @@ Delegates.RoundedItemDelegate {
enabled: false
implicitWidth: Kirigami.Units.iconSizes.smallMedium
implicitHeight: Kirigami.Units.iconSizes.smallMedium
visible: currentRoom.pushNotificationState === PushNotificationState.Mute && !configButton.visible
visible: currentRoom.pushNotificationState === PushNotificationState.Mute && !configButton.visible && !root.collapsed
Accessible.name: i18n("Muted room")
Layout.rightMargin: Kirigami.Units.smallSpacing
}
@@ -114,7 +107,7 @@ Delegates.RoundedItemDelegate {
id: notificationCountLabel
text: currentRoom.pushNotificationState === PushNotificationState.MentionKeyword || currentRoom.isLowPriority ? root.highlightCount : root.notificationCount
visible: root.hasNotifications && currentRoom.pushNotificationState !== PushNotificationState.Mute
visible: root.hasNotifications && currentRoom.pushNotificationState !== PushNotificationState.Mute && !root.collapsed
color: Kirigami.Theme.textColor
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
@@ -138,7 +131,7 @@ Delegates.RoundedItemDelegate {
QQC2.Button {
id: configButton
visible: root.hovered && !Kirigami.Settings.isMobile && !Config.compactRoomList
visible: root.hovered && !Kirigami.Settings.isMobile && !Config.compactRoomList && !root.collapsed
text: i18n("Configure room")
display: QQC2.Button.IconOnly
@@ -147,11 +140,6 @@ Delegates.RoundedItemDelegate {
}
}
function select() {
RoomManager.resolveResource(currentRoom.id);
root.selected();
}
function createRoomListContextMenu() {
const component = Qt.createComponent("qrc:/org/kde/neochat/qml/ContextMenu.qml");
if (component.status === Component.Error) {

View File

@@ -6,6 +6,7 @@ import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import QtQml.Models
import Qt.labs.qmlmodels
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.components as KirigamiComponents
@@ -24,50 +25,41 @@ Kirigami.Page {
* @note Other objects can access the value but the private function makes sure
* that only the internal members can modify it.
*/
readonly property int currentWidth: _private.currentWidth + spaceListWidth
readonly property int currentWidth: _private.currentWidth + spaceListWidth + 1
readonly property alias spaceListWidth: spaceDrawer.width
required property NeoChatConnection connection
readonly property RoomListModel roomListModel: RoomListModel {
readonly property RoomTreeModel roomTreeModel: RoomTreeModel {
connection: root.connection
}
property bool spaceChanging: false
property bool spaceChanging: true
readonly property bool collapsed: Config.collapsed
property var enteredRoom: null
onCollapsedChanged: if (collapsed) {
sortFilterRoomListModel.filterText = "";
}
Component.onCompleted: Runner.roomListModel = root.roomListModel
Connections {
target: RoomManager
function onCurrentRoomChanged() {
itemSelection.setCurrentIndex(roomListModel.index(roomListModel.rowForRoom(RoomManager.currentRoom), 0), ItemSelectionModel.SelectCurrent);
onCollapsedChanged: {
if (collapsed) {
sortFilterRoomTreeModel.filterText = "";
}
}
function goToNextRoomFiltered(condition) {
let index = listView.currentIndex;
while (index++ !== listView.count - 1) {
if (condition(listView.itemAtIndex(index))) {
listView.currentIndex = index;
listView.currentItem.clicked();
let index = treeView.currentIndex;
while (index++ !== treeView.count - 1) {
if (condition(treeView.itemAtIndex(index))) {
treeView.currentIndex = index;
treeView.currentItem.clicked();
return;
}
}
}
function goToPreviousRoomFiltered(condition) {
let index = listView.currentIndex;
let index = treeView.currentIndex;
while (index-- !== 0) {
if (condition(listView.itemAtIndex(index))) {
listView.currentIndex = index;
listView.currentItem.clicked();
if (condition(treeView.itemAtIndex(index))) {
treeView.currentIndex = index;
treeView.currentItem.clicked();
return;
}
}
@@ -108,7 +100,7 @@ Kirigami.Page {
connection: root.connection
onSelectionChanged: root.spaceChanging = true
onSpacesUpdated: sortFilterRoomListModel.invalidate()
onSpacesUpdated: sortFilterRoomTreeModel.invalidate()
}
Kirigami.Separator {
@@ -126,213 +118,151 @@ Kirigami.Page {
Kirigami.Theme.colorSet: Kirigami.Theme.View
}
ListView {
id: listView
activeFocusOnTab: true
clip: true
Keys.onDownPressed: ; // Do not delete 🫠
Keys.onUpPressed: ; // These make sure the scrollview doesn't also scroll while going through the roomlist using the arrow keys
contentItem: TreeView {
id: treeView
topMargin: Math.round(Kirigami.Units.smallSpacing / 2)
header: QQC2.ItemDelegate {
width: visible ? ListView.view.width : 0
height: visible ? Kirigami.Units.gridUnit * 2 : 0
clip: true
reuseItems: false
visible: root.collapsed
topPadding: Kirigami.Units.largeSpacing
leftPadding: Kirigami.Units.largeSpacing
rightPadding: Kirigami.Units.largeSpacing
bottomPadding: Kirigami.Units.largeSpacing
onClicked: quickView.item.open()
Kirigami.Icon {
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.smallMedium
height: Kirigami.Units.iconSizes.smallMedium
source: "search"
onLayoutChanged: {
if (sortFilterRoomTreeModel.filterTextJustChanged) {
treeView.expandRecursively();
sortFilterRoomTreeModel.filterTextJustChanged = false;
}
Kirigami.Separator {
width: parent.width
anchors.bottom: parent.bottom
}
}
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
width: parent.width - (Kirigami.Units.largeSpacing * 4)
visible: listView.count == 0
text: if (sortFilterRoomListModel.filterText.length > 0) {
return spaceDrawer.showDirectChats ? i18n("No friends found") : i18n("No rooms found");
} else {
return spaceDrawer.showDirectChats ? i18n("You haven't added any of your friends yet, click below to search for them.") : i18n("Join some rooms to get started");
}
helpfulAction: spaceDrawer.showDirectChats ? userSearchAction : exploreRoomAction
Kirigami.Action {
id: exploreRoomAction
icon.name: sortFilterRoomListModel.filterText.length > 0 ? "search" : "list-add"
text: sortFilterRoomListModel.filterText.length > 0 ? i18n("Search in room directory") : i18n("Explore rooms")
onTriggered: {
let dialog = pageStack.layers.push("qrc:/org/kde/neochat/qml/ExploreRoomsPage.qml", {
connection: root.connection,
keyword: sortFilterRoomListModel.filterText
}, {
title: i18nc("@title", "Explore Rooms")
});
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
RoomManager.resolveResource(roomId.length > 0 ? roomId : alias, isJoined ? "" : "join");
});
if (root.spaceChanging) {
treeView.expandRecursively();
if (spaceDrawer.showDirectChats || spaceDrawer.selectedSpaceId.length < 1) {
RoomManager.resolveResource(treeView.itemAtIndex(treeView.index(1, 0)).currentRoom.id);
}
}
Kirigami.Action {
id: userSearchAction
icon.name: sortFilterRoomListModel.filterText.length > 0 ? "search" : "list-add"
text: sortFilterRoomListModel.filterText.length > 0 ? i18n("Search in friend directory") : i18n("Find your friends")
onTriggered: pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/UserSearchPage.qml", {
connection: root.connection
}, {
title: i18nc("@title", "Find your friends")
})
}
}
ItemSelectionModel {
id: itemSelection
model: root.roomListModel
onCurrentChanged: (current, previous) => listView.currentIndex = sortFilterRoomListModel.mapFromSource(current).row
}
model: SortFilterRoomListModel {
id: sortFilterRoomListModel
sourceModel: root.roomListModel
roomSortOrder: SortFilterRoomListModel.Categories
onLayoutChanged: {
layoutTimer.restart();
listView.currentIndex = sortFilterRoomListModel.mapFromSource(itemSelection.currentIndex).row;
}
activeSpaceId: spaceDrawer.selectedSpaceId
mode: spaceDrawer.showDirectChats ? SortFilterRoomListModel.DirectChats : SortFilterRoomListModel.Rooms
}
// HACK: This is the only way to guarantee the correct choice when
// there are multiple property changes that invalidate the filter. I.e.
// in this case activeSpaceId followed by mode.
Timer {
id: layoutTimer
interval: 300
onTriggered: if ((spaceDrawer.showDirectChats || spaceDrawer.selectedSpaceId.length < 1) && root.spaceChanging) {
RoomManager.resolveResource(listView.itemAtIndex(0).currentRoom.id);
root.spaceChanging = false;
}
}
section {
property: "category"
delegate: root.collapsed ? foldButton : sectionHeader
model: SortFilterRoomTreeModel {
id: sortFilterRoomTreeModel
property bool filterTextJustChanged: false
sourceModel: root.roomTreeModel
roomSortOrder: SortFilterRoomTreeModel.Categories
activeSpaceId: spaceDrawer.selectedSpaceId
mode: spaceDrawer.showDirectChats ? SortFilterRoomTreeModel.DirectChats : SortFilterRoomTreeModel.Rooms
}
Component {
id: sectionHeader
Kirigami.ListSectionHeader {
height: implicitHeight
width: listView.width
label: roomListModel.categoryName(section)
action: Kirigami.Action {
onTriggered: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section))
}
selectionModel: ItemSelectionModel {}
QQC2.ToolButton {
icon {
name: roomListModel.categoryVisible(section) ? "go-up" : "go-down"
width: Kirigami.Units.iconSizes.small
height: Kirigami.Units.iconSizes.small
}
text: roomListModel.categoryVisible(section) ? i18nc("Collapse <section name>", "Collapse %1", roomListModel.categoryName(section)) : i18nc("Expand <section name", "Expand %1", roomListModel.categoryName(section))
display: QQC2.Button.IconOnly
delegate: DelegateChooser {
role: "delegateType"
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
onClicked: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section))
DelegateChoice {
roleValue: "section"
delegate: RoomTreeSection {
collapsed: root.collapsed
}
}
}
Component {
id: foldButton
Item {
width: ListView.view.width
height: visible ? width : 0
QQC2.ToolButton {
id: button
anchors.centerIn: parent
icon {
name: hovered ? (roomListModel.categoryVisible(section) ? "go-up" : "go-down") : roomListModel.categoryIconName(section)
DelegateChoice {
roleValue: "normal"
delegate: RoomDelegate {
id: roomDelegate
required property int row
required property TreeView treeView
required property bool current
onCurrentChanged: if (current) {
forceActiveFocus(Qt.TabFocusReason)
}
implicitWidth: treeView.width
connection: root.connection
collapsed: root.collapsed
highlighted: RoomManager.currentRoom === currentRoom
}
}
DelegateChoice {
roleValue: "search"
delegate: Delegates.RoundedItemDelegate {
required property TreeView treeView
implicitWidth: treeView.width
onClicked: quickView.item.open()
contentItem: Kirigami.Icon {
width: Kirigami.Units.iconSizes.smallMedium
height: Kirigami.Units.iconSizes.smallMedium
source: "search"
}
onClicked: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section))
QQC2.ToolTip.text: roomListModel.categoryName(section)
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
}
reuseItems: true
currentIndex: -1 // we don't want any room highlighted by default
DelegateChoice {
roleValue: "addDirect"
delegate: Delegates.RoundedItemDelegate {
text: i18n("Find your friends")
icon.name: "list-add-user"
icon.width: Kirigami.Units.gridUnit * 2
icon.height: Kirigami.Units.gridUnit * 2
delegate: root.collapsed ? collapsedModeListComponent : normalModeListComponent
Component {
id: collapsedModeListComponent
CollapsedRoomDelegate {
filterText: sortFilterRoomListModel.filterText
onClicked: pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/UserSearchPage.qml", {
connection: root.connection
}, {
title: i18nc("@title", "Find your friends")
})
}
}
}
Component {
id: normalModeListComponent
RoomDelegate {
filterText: sortFilterRoomListModel.filterText
connection: root.connection
height: visible ? implicitHeight : 0
visible: categoryVisible || filterText.length > 0
}
}
footer: Delegates.RoundedItemDelegate {
visible: listView.view.count > 0 && spaceDrawer.showDirectChats
text: i18n("Find your friends")
icon.name: "list-add-user"
icon.width: Kirigami.Units.gridUnit * 2
icon.height: Kirigami.Units.gridUnit * 2
onClicked: pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/UserSearchPage.qml", {
connection: root.connection
}, {
title: i18nc("@title", "Find your friends")
})
}
}
}
}
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
anchors.horizontalCenterOffset: (spaceDrawer.width + 1) / 2
width: scrollView.width - Kirigami.Units.largeSpacing * 4
visible: treeView.rows == 0
text: if (sortFilterRoomTreeModel.filterText.length > 0) {
return spaceDrawer.showDirectChats ? i18n("No friends found") : i18n("No rooms found");
} else {
return spaceDrawer.showDirectChats ? i18n("You haven't added any of your friends yet, click below to search for them.") : i18n("Join some rooms to get started");
}
helpfulAction: spaceDrawer.showDirectChats ? userSearchAction : exploreRoomAction
Kirigami.Action {
id: exploreRoomAction
icon.name: sortFilterRoomTreeModel.filterText.length > 0 ? "search" : "list-add"
text: sortFilterRoomTreeModel.filterText.length > 0 ? i18n("Search in room directory") : i18n("Explore rooms")
onTriggered: {
let dialog = pageStack.layers.push("qrc:/org/kde/neochat/qml/ExploreRoomsPage.qml", {
connection: root.connection,
keyword: sortFilterRoomTreeModel.filterText
}, {
title: i18nc("@title", "Explore Rooms")
});
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
RoomManager.resolveResource(roomId.length > 0 ? roomId : alias, isJoined ? "" : "join");
});
}
}
Kirigami.Action {
id: userSearchAction
icon.name: sortFilterRoomTreeModel.filterText.length > 0 ? "search" : "list-add"
text: sortFilterRoomTreeModel.filterText.length > 0 ? i18n("Search in friend directory") : i18n("Find your friends")
onTriggered: pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/UserSearchPage.qml", {
connection: root.connection
}, {
title: i18nc("@title", "Find your friends")
})
}
}
footer: Loader {
width: parent.width
active: !root.collapsed
sourceComponent: Kirigami.Settings.isMobile ? exploreComponentMobile : userInfoDesktop
}
@@ -404,7 +334,8 @@ Kirigami.Page {
connection: root.connection
onTextChanged: newText => {
sortFilterRoomListModel.filterText = newText;
sortFilterRoomTreeModel.filterText = newText;
sortFilterRoomTreeModel.filterTextJustChanged = true;
}
}
}
@@ -415,7 +346,7 @@ Kirigami.Page {
connection: root.connection
onTextChanged: newText => {
sortFilterRoomListModel.filterText = newText;
sortFilterRoomTreeModel.filterText = newText;
}
}
}
@@ -429,6 +360,6 @@ Kirigami.Page {
property int currentWidth: Config.collapsed ? collapsedSize : defaultWidth
readonly property int defaultWidth: Kirigami.Units.gridUnit * 17
readonly property int collapseWidth: Kirigami.Units.gridUnit * 10
readonly property int collapsedSize: Kirigami.Units.gridUnit * 3 - Kirigami.Units.smallSpacing * 3 + (scrollView.QQC2.ScrollBar.vertical.visible ? scrollView.QQC2.ScrollBar.vertical.width : 0)
readonly property int collapsedSize: Kirigami.Units.gridUnit + (Config.compactRoomList ? 0 : Kirigami.Units.largeSpacing * 2) + Kirigami.Units.largeSpacing * 2 + (scrollView.QQC2.ScrollBar.vertical.visible ? scrollView.QQC2.ScrollBar.vertical.width : 0)
}
}

View File

@@ -212,12 +212,7 @@ Kirigami.Page {
}
Keys.onPressed: event => {
if (!(event.modifiers & Qt.ControlModifier) && event.key < Qt.Key_Escape) {
event.accepted = true;
chatBarLoader.item.insertText(event.text);
chatBarLoader.item.forceActiveFocus();
return;
} else if (event.key === Qt.Key_PageUp) {
if (event.key === Qt.Key_PageUp) {
event.accepted = true;
timelineViewLoader.item.pageUp();
} else if (event.key === Qt.Key_PageDown) {

View File

@@ -0,0 +1,83 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
QQC2.ItemDelegate {
id: root
required property TreeView treeView
required property bool isTreeNode
required property bool expanded
required property bool hasChildren
required property int depth
required property string displayName
required property int row
required property bool current
onCurrentChanged: if (current) {
collapseButton.forceActiveFocus(Qt.TabFocusReason)
}
required property bool selected
property bool collapsed: false
implicitWidth: treeView.width
hoverEnabled: false
activeFocusOnTab: false
background: null
onClicked: root.treeView.toggleExpanded(row)
Keys.onEnterPressed: root.treeView.toggleExpanded(row)
Keys.onReturnPressed: root.treeView.toggleExpanded(row)
Keys.onSpacePressed: root.treeView.toggleExpanded(row)
contentItem: RowLayout {
spacing: Kirigami.Units.largeSpacing
Kirigami.Heading {
Layout.alignment: Qt.AlignVCenter
visible: !root.collapsed
opacity: 0.7
level: 5
type: Kirigami.Heading.Primary
text: root.collapsed ? "" : model.displayName
elide: Text.ElideRight
// we override the Primary type's font weight (DemiBold) for Bold for contrast with small text
font.weight: Font.Bold
}
Kirigami.Separator {
Layout.fillWidth: true
visible: !root.collapsed
Layout.alignment: Qt.AlignVCenter
}
QQC2.ToolButton {
id: collapseButton
Layout.alignment: Qt.AlignHCenter
icon {
name: root.expanded ? "go-up" : "go-down"
width: Kirigami.Units.iconSizes.small
height: Kirigami.Units.iconSizes.small
}
text: root.expanded ? i18nc("Collapse <section name>", "Collapse %1", root.displayName) : i18nc("Expand <section name", "Expand %1", root.displayName)
display: QQC2.Button.IconOnly
activeFocusOnTab: false
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
onClicked: root.treeView.toggleExpanded(row)
}
}
}

View File

@@ -5,7 +5,9 @@
#include <QDBusMetaType>
#include "controller.h"
#include "neochatroom.h"
#include "roomlistmodel.h"
#include "roommanager.h"
#include "windowcontroller.h"
@@ -27,6 +29,12 @@ RemoteImage Runner::serializeImage(const QImage &image)
Runner::Runner()
: QObject()
{
m_sourceModel = new RoomListModel(this);
m_model.setSourceModel(m_sourceModel);
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, [this]() {
m_sourceModel->setConnection(Controller::instance().activeConnection());
});
qDBusRegisterMetaType<RemoteMatch>();
qDBusRegisterMetaType<RemoteMatches>();
qDBusRegisterMetaType<RemoteAction>();

View File

@@ -204,6 +204,6 @@ private:
RemoteImage serializeImage(const QImage &image);
SortFilterRoomListModel m_model;
RoomListModel m_sourceModel;
RoomListModel *m_sourceModel;
Runner();
};

View File

@@ -10,6 +10,7 @@
#include <KSharedConfig>
#include "neochatroom.h"
#include "neochatroomtype.h"
#include "roomlistmodel.h"
using namespace Quotient;
@@ -120,7 +121,7 @@ qsizetype SpaceHierarchyCache::notificationCountForSpace(const QString &spaceId)
for (const auto &childId : children) {
if (const auto child = static_cast<NeoChatRoom *>(m_connection->room(childId))) {
auto category = RoomListModel::category(child);
auto category = NeoChatRoomType::typeForRoom(child);
if (!added.contains(child->id()) && child->successorId().isEmpty()) {
switch (category) {
case NeoChatRoomType::Normal: