Port RoomList to TreeView
Use a tree model for the room list closes network/neochat#156 BUG: 456643
This commit is contained in:
committed by
James Graham
parent
dae23ccd4b
commit
fc6ea0b779
@@ -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
|
||||
|
||||
97
src/enums/neochatroomtype.h
Normal file
97
src/enums/neochatroomtype.h
Normal 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");
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
323
src/models/roomtreemodel.cpp
Normal file
323
src/models/roomtreemodel.cpp
Normal 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"
|
||||
94
src/models/roomtreemodel.h
Normal file
94
src/models/roomtreemodel.h
Normal 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 = {});
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
161
src/models/sortfilterroomtreemodel.cpp
Normal file
161
src/models/sortfilterroomtreemodel.cpp
Normal 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"
|
||||
111
src/models/sortfilterroomtreemodel.h
Normal file
111
src/models/sortfilterroomtreemodel.h
Normal 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;
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -1279,7 +1279,7 @@ void NeoChatRoom::removeParent(const QString &parentId)
|
||||
}
|
||||
}
|
||||
|
||||
bool NeoChatRoom::isSpace()
|
||||
bool NeoChatRoom::isSpace() const
|
||||
{
|
||||
const auto creationEvent = this->creation();
|
||||
if (!creationEvent) {
|
||||
|
||||
@@ -531,7 +531,7 @@ public:
|
||||
*/
|
||||
Q_INVOKABLE void removeParent(const QString &parentId);
|
||||
|
||||
[[nodiscard]] bool isSpace();
|
||||
[[nodiscard]] bool isSpace() const;
|
||||
|
||||
qsizetype childrenNotificationCount();
|
||||
|
||||
|
||||
@@ -30,8 +30,7 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
delegate: RoomDelegate {
|
||||
id: roomDelegate
|
||||
filterText: ""
|
||||
onSelected: {
|
||||
onClicked: {
|
||||
root.chosen(roomDelegate.currentRoom.id);
|
||||
}
|
||||
connection: root.connection
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -90,9 +90,8 @@ QQC2.Dialog {
|
||||
}
|
||||
|
||||
delegate: RoomDelegate {
|
||||
filterText: searchField.text
|
||||
connection: root.connection
|
||||
onSelected: root.close()
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
83
src/qml/RoomTreeSection.qml
Normal file
83
src/qml/RoomTreeSection.qml
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
|
||||
@@ -204,6 +204,6 @@ private:
|
||||
RemoteImage serializeImage(const QImage &image);
|
||||
|
||||
SortFilterRoomListModel m_model;
|
||||
RoomListModel m_sourceModel;
|
||||
RoomListModel *m_sourceModel;
|
||||
Runner();
|
||||
};
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user