Move ChatDocumentHandler and related includes to LibNeoChat

This commit is contained in:
James Graham
2025-04-13 15:40:19 +01:00
parent 4fe9c76d90
commit bffd7fb13d
21 changed files with 81 additions and 60 deletions

View File

@@ -0,0 +1,208 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "completionmodel.h"
#include <QDebug>
#include "completionproxymodel.h"
#include "models/actionsmodel.h"
#include "models/customemojimodel.h"
#include "models/emojimodel.h"
#include "neochatroom.h"
#include "userlistmodel.h"
CompletionModel::CompletionModel(QObject *parent)
: QAbstractListModel(parent)
, m_filterModel(new CompletionProxyModel())
, m_emojiModel(new QConcatenateTablesProxyModel(this))
{
connect(this, &CompletionModel::textChanged, this, &CompletionModel::updateCompletion);
m_emojiModel->addSourceModel(&CustomEmojiModel::instance());
m_emojiModel->addSourceModel(&EmojiModel::instance());
}
QString CompletionModel::text() const
{
return m_text;
}
void CompletionModel::setText(const QString &text, const QString &fullText)
{
m_text = text;
m_fullText = fullText;
Q_EMIT textChanged();
}
int CompletionModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
if (m_autoCompletionType == None) {
return 0;
}
return m_filterModel->rowCount();
}
QVariant CompletionModel::data(const QModelIndex &index, int role) const
{
if (index.row() < 0 || index.row() >= m_filterModel->rowCount()) {
return {};
}
auto filterIndex = m_filterModel->index(index.row(), 0);
if (m_autoCompletionType == User) {
if (role == DisplayNameRole) {
return m_filterModel->data(filterIndex, UserListModel::DisplayNameRole);
}
if (role == SubtitleRole) {
return m_filterModel->data(filterIndex, UserListModel::UserIdRole);
}
if (role == IconNameRole) {
return m_filterModel->data(filterIndex, UserListModel::AvatarRole);
}
}
if (m_autoCompletionType == Command) {
if (role == DisplayNameRole) {
return u"%1 %2"_s.arg(m_filterModel->data(filterIndex, ActionsModel::Prefix).toString(),
m_filterModel->data(filterIndex, ActionsModel::Parameters).toString());
}
if (role == SubtitleRole) {
return m_filterModel->data(filterIndex, ActionsModel::Description);
}
if (role == IconNameRole) {
return u"invalid"_s;
}
if (role == ReplacedTextRole) {
return m_filterModel->data(filterIndex, ActionsModel::Prefix);
}
}
if (m_autoCompletionType == Room) {
if (role == DisplayNameRole) {
return m_filterModel->data(filterIndex, RoomListModel::DisplayNameRole);
}
if (role == SubtitleRole) {
return m_filterModel->data(filterIndex, RoomListModel::CanonicalAliasRole);
}
if (role == IconNameRole) {
return m_filterModel->data(filterIndex, RoomListModel::AvatarRole).toString();
}
}
if (m_autoCompletionType == Emoji) {
if (role == DisplayNameRole) {
return m_filterModel->data(filterIndex, CustomEmojiModel::DisplayRole);
}
if (role == IconNameRole) {
return m_filterModel->data(filterIndex, CustomEmojiModel::MxcUrl);
}
if (role == ReplacedTextRole) {
return m_filterModel->data(filterIndex, CustomEmojiModel::ReplacedTextRole);
}
if (role == SubtitleRole) {
return m_filterModel->data(filterIndex, EmojiModel::DescriptionRole);
}
}
return {};
}
QHash<int, QByteArray> CompletionModel::roleNames() const
{
return {
{DisplayNameRole, "displayName"},
{SubtitleRole, "subtitle"},
{IconNameRole, "iconName"},
{ReplacedTextRole, "replacedText"},
};
}
void CompletionModel::updateCompletion()
{
if (text().startsWith(QLatin1Char('@'))) {
m_filterModel->setSourceModel(m_userListModel);
m_filterModel->setFilterRole(UserListModel::UserIdRole);
m_filterModel->setSecondaryFilterRole(UserListModel::DisplayNameRole);
m_filterModel->setFullText(m_fullText);
m_filterModel->setFilterText(m_text);
m_autoCompletionType = User;
m_filterModel->invalidate();
} else if (text().startsWith(QLatin1Char('/'))) {
m_filterModel->setSourceModel(&ActionsModel::instance());
m_filterModel->setFilterRole(ActionsModel::Prefix);
m_filterModel->setSecondaryFilterRole(-1);
m_filterModel->setFullText(m_fullText);
m_filterModel->setFilterText(m_text.mid(1));
m_autoCompletionType = Command;
m_filterModel->invalidate();
} else if (text().startsWith(QLatin1Char('#'))) {
m_autoCompletionType = Room;
m_filterModel->setSourceModel(m_roomListModel);
m_filterModel->setFilterRole(RoomListModel::CanonicalAliasRole);
m_filterModel->setSecondaryFilterRole(RoomListModel::DisplayNameRole);
m_filterModel->setFullText(m_fullText);
m_filterModel->setFilterText(m_text);
m_filterModel->invalidate();
} else if (text().startsWith(QLatin1Char(':')) && text().size() > 1 && !text()[1].isUpper()
&& (m_fullText.indexOf(QLatin1Char(':'), 1) == -1
|| (m_fullText.indexOf(QLatin1Char(' ')) != -1 && m_fullText.indexOf(QLatin1Char(':'), 1) > m_fullText.indexOf(QLatin1Char(' '), 1)))) {
m_filterModel->setSourceModel(m_emojiModel);
m_autoCompletionType = Emoji;
m_filterModel->setFilterRole(CustomEmojiModel::Name);
m_filterModel->setSecondaryFilterRole(EmojiModel::DescriptionRole);
m_filterModel->setFullText(m_fullText);
m_filterModel->setFilterText(m_text);
m_filterModel->invalidate();
} else {
m_autoCompletionType = None;
}
beginResetModel();
endResetModel();
}
NeoChatRoom *CompletionModel::room() const
{
return m_room;
}
void CompletionModel::setRoom(NeoChatRoom *room)
{
m_room = room;
Q_EMIT roomChanged();
}
CompletionModel::AutoCompletionType CompletionModel::autoCompletionType() const
{
return m_autoCompletionType;
}
void CompletionModel::setAutoCompletionType(AutoCompletionType autoCompletionType)
{
m_autoCompletionType = autoCompletionType;
Q_EMIT autoCompletionTypeChanged();
}
RoomListModel *CompletionModel::roomListModel() const
{
return m_roomListModel;
}
void CompletionModel::setRoomListModel(RoomListModel *roomListModel)
{
m_roomListModel = roomListModel;
Q_EMIT roomListModelChanged();
}
UserListModel *CompletionModel::userListModel() const
{
return m_userListModel;
}
void CompletionModel::setUserListModel(UserListModel *userListModel)
{
if (userListModel == m_userListModel) {
return;
}
m_userListModel = userListModel;
Q_EMIT userListModelChanged();
}
#include "moc_completionmodel.cpp"

View File

@@ -0,0 +1,139 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <QConcatenateTablesProxyModel>
#include <QQmlEngine>
#include <QSortFilterProxyModel>
#include "roomlistmodel.h"
class CompletionProxyModel;
class UserListModel;
class NeoChatRoom;
class RoomListModel;
/**
* @class CompletionModel
*
* This class defines the model for suggesting completions in chat text.
*
* This model is able to select the appropriate completion type for the input text
* and present a list of options that can be presented to the user.
*/
class CompletionModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The current text to search for completions.
*/
Q_PROPERTY(QString text READ text NOTIFY textChanged)
/**
* @brief The current room that the model is getting completions for.
*/
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
/**
* @brief The current type of completion being done on the entered text.
*
* @sa AutoCompletionType
*/
Q_PROPERTY(AutoCompletionType autoCompletionType READ autoCompletionType NOTIFY autoCompletionTypeChanged)
/**
* @brief The RoomListModel to be used for room completions.
*/
Q_PROPERTY(RoomListModel *roomListModel READ roomListModel WRITE setRoomListModel NOTIFY roomListModelChanged)
/**
* @brief The UserListModel to be used for room completions.
*/
Q_PROPERTY(UserListModel *userListModel READ userListModel WRITE setUserListModel NOTIFY userListModelChanged)
public:
/**
* @brief Defines the different types of completion available.
*/
enum AutoCompletionType {
User, /**< A user in the current room. */
Room, /**< A matrix room. */
Emoji, /**< An emoji. */
Command, /**< A / command. */
None, /**< No available completion for the current text. */
};
Q_ENUM(AutoCompletionType)
/**
* @brief Defines the model roles.
*/
enum Roles {
DisplayNameRole = Qt::DisplayRole, /**< The main text to show. */
SubtitleRole, /**< The subtitle text to show. */
IconNameRole, /**< The icon to show. */
ReplacedTextRole, /**< The text to replace the input text with for the completion. */
};
Q_ENUM(Roles)
explicit CompletionModel(QObject *parent = nullptr);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa EventRoles, QAbstractItemModel::roleNames()
*/
QHash<int, QByteArray> roleNames() const override;
QString text() const;
void setText(const QString &text, const QString &fullText);
NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
RoomListModel *roomListModel() const;
void setRoomListModel(RoomListModel *roomListModel);
UserListModel *userListModel() const;
void setUserListModel(UserListModel *userListModel);
AutoCompletionType autoCompletionType() const;
void setAutoCompletionType(AutoCompletionType autoCompletionType);
Q_SIGNALS:
void textChanged();
void roomChanged();
void autoCompletionTypeChanged();
void roomListModelChanged();
void userListModelChanged();
private:
QString m_text;
QString m_fullText;
CompletionProxyModel *m_filterModel;
QPointer<NeoChatRoom> m_room;
AutoCompletionType m_autoCompletionType = None;
void updateCompletion();
UserListModel *m_userListModel;
RoomListModel *m_roomListModel;
QConcatenateTablesProxyModel *m_emojiModel;
};
Q_DECLARE_METATYPE(CompletionModel::AutoCompletionType);

View File

@@ -0,0 +1,64 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "completionproxymodel.h"
#include <QDebug>
bool CompletionProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
Q_UNUSED(sourceParent);
if (m_filterText.isEmpty()) {
return false;
}
if (sourceModel()->data(sourceModel()->index(sourceRow, 0), filterRole()).toString().isEmpty()) {
return false;
}
return (sourceModel()->data(sourceModel()->index(sourceRow, 0), filterRole()).toString().startsWith(m_filterText, Qt::CaseInsensitive)
&& !m_fullText.startsWith(sourceModel()->data(sourceModel()->index(sourceRow, 0), filterRole()).toString()))
|| (m_secondaryFilterRole != -1
&& sourceModel()
->data(sourceModel()->index(sourceRow, 0), secondaryFilterRole())
.toString()
.startsWith(QStringView(m_filterText).sliced(1), Qt::CaseInsensitive));
}
bool CompletionProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
{
if (m_secondaryFilterRole == -1)
return QSortFilterProxyModel::lessThan(source_left, source_right);
bool left_primary = sourceModel()->data(source_left, filterRole()).toString().startsWith(m_filterText, Qt::CaseInsensitive);
bool right_primary = sourceModel()->data(source_right, filterRole()).toString().startsWith(m_filterText, Qt::CaseInsensitive);
if (left_primary != right_primary)
return left_primary;
return QSortFilterProxyModel::lessThan(source_left, source_right);
}
int CompletionProxyModel::secondaryFilterRole() const
{
return m_secondaryFilterRole;
}
void CompletionProxyModel::setSecondaryFilterRole(int role)
{
m_secondaryFilterRole = role;
}
QString CompletionProxyModel::filterText() const
{
return m_filterText;
}
void CompletionProxyModel::setFilterText(const QString &filterText)
{
m_filterText = filterText;
}
void CompletionProxyModel::setFullText(const QString &fullText)
{
m_fullText = fullText;
}
#include "moc_completionproxymodel.cpp"

View File

@@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <QSortFilterProxyModel>
/**
* @class CompletionProxyModel
*
* A filter model to sort and filter completion results.
*
* This model is designed to work with multiple source models depending upon the
* completion type.
*
* A model value will be shown if its primary or secondary role values start with
* the filter text. The exception is if the full text perfectly matches
* the primary filter role value in which case the completion ends (i.e. the filter
* will return no results).
*
* @note The filter is primarily design to work with strings, therefore make sure
* that the source model roles that are to be filtered are strings.
*/
class CompletionProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
/**
* @brief Wether a row should be shown or not.
*
* @sa QSortFilterProxyModel::filterAcceptsRow
*/
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
/**
* @brief Returns true if the value of source_left is less than source_right.
*
* @sa QSortFilterProxyModel::lessThan
*/
bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override;
/**
* @brief Get the current secondary filter role.
*/
int secondaryFilterRole() const;
/**
* @brief Set the secondary filter role.
*
* Refer to the source model for what value corresponds to what role.
*/
void setSecondaryFilterRole(int role);
/**
* @brief Get the current text being used to filter the source model.
*/
QString filterText() const;
/**
* @brief Set the text to be used to filter the source model.
*/
void setFilterText(const QString &filterText);
/**
* @brief Set the full text in the chatbar after the completion start.
*
* This is used to automatically end the completion if the user replicated the
* primary filter role value perfectly.
*/
void setFullText(const QString &fullText);
private:
int m_secondaryFilterRole = -1;
QString m_filterText;
QString m_fullText;
};

View File

@@ -0,0 +1,328 @@
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
#include "roomlistmodel.h"
#include <Quotient/events/roommemberevent.h>
#include "enums/neochatroomtype.h"
#include "eventhandler.h"
#include "neochatconnection.h"
#include "neochatroom.h"
#include "spacehierarchycache.h"
#include <KLocalizedString>
using namespace Quotient;
Q_DECLARE_METATYPE(Quotient::JoinState)
std::function<bool(const Quotient::RoomEvent *)> RoomListModel::m_hiddenFilter = [](const Quotient::RoomEvent *) -> bool {
return false;
};
RoomListModel::RoomListModel(QObject *parent)
: QAbstractListModel(parent)
{
connect(&SpaceHierarchyCache::instance(), &SpaceHierarchyCache::spaceHierarchyChanged, this, [this]() {
Q_EMIT dataChanged(index(0, 0), index(rowCount(), 0), {IsChildSpaceRole});
});
}
RoomListModel::~RoomListModel() = default;
NeoChatConnection *RoomListModel::connection() const
{
return m_connection;
}
void RoomListModel::setConnection(NeoChatConnection *connection)
{
if (connection == m_connection) {
return;
}
if (m_connection) {
m_connection->disconnect(this);
}
if (!connection) {
qDebug() << "Removing current connection...";
m_connection = nullptr;
beginResetModel();
m_rooms.clear();
endResetModel();
return;
}
m_connection = connection;
for (NeoChatRoom *room : std::as_const(m_rooms)) {
room->disconnect(this);
}
connect(connection, &Connection::connected, this, &RoomListModel::doResetModel);
connect(connection, &Connection::invitedRoom, this, &RoomListModel::updateRoom);
connect(connection, &Connection::joinedRoom, this, &RoomListModel::updateRoom);
connect(connection, &Connection::leftRoom, this, &RoomListModel::updateRoom);
connect(connection, &Connection::aboutToDeleteRoom, this, &RoomListModel::deleteRoom);
connect(connection, &Connection::directChatsListChanged, this, [this, connection](Quotient::DirectChatsMap additions, Quotient::DirectChatsMap removals) {
auto refreshRooms = [this, &connection](Quotient::DirectChatsMap rooms) {
for (const QString &roomID : std::as_const(rooms)) {
auto room = connection->room(roomID);
if (room) {
refresh(static_cast<NeoChatRoom *>(room));
}
}
};
refreshRooms(std::move(additions));
refreshRooms(std::move(removals));
});
doResetModel();
Q_EMIT connectionChanged();
}
void RoomListModel::doResetModel()
{
beginResetModel();
m_rooms.clear();
const auto rooms = m_connection->allRooms();
for (const auto &room : rooms) {
doAddRoom(room);
}
endResetModel();
}
NeoChatRoom *RoomListModel::roomAt(int row) const
{
return m_rooms.at(row);
}
void RoomListModel::doAddRoom(Room *r)
{
if (auto room = static_cast<NeoChatRoom *>(r)) {
m_rooms.append(room);
connectRoomSignals(room);
Q_EMIT roomAdded(room);
} else {
qCritical() << "Attempt to add nullptr to the room list";
Q_ASSERT(false);
}
}
void RoomListModel::connectRoomSignals(NeoChatRoom *room)
{
connect(room, &Room::displaynameChanged, this, [this, room] {
refresh(room, {DisplayNameRole});
});
connect(room, &Room::unreadStatsChanged, this, [this, room] {
refresh(room, {ContextNotificationCountRole, HasHighlightNotificationsRole});
});
connect(room, &Room::notificationCountChanged, this, [this, room] {
refresh(room);
});
connect(room, &Room::highlightCountChanged, this, [this, room] {
refresh(room);
});
connect(room, &Room::avatarChanged, this, [this, room] {
refresh(room, {AvatarRole});
});
connect(room, &Room::tagsChanged, this, [this, room] {
refresh(room);
});
connect(room, &Room::joinStateChanged, this, [this, room] {
refresh(room);
});
connect(room, &Room::addedMessages, this, [this, room] {
refresh(room, {SubtitleTextRole});
});
connect(room, &Room::pendingEventMerged, this, [this, room] {
refresh(room, {SubtitleTextRole});
});
}
void RoomListModel::updateRoom(Room *room, Room *prev)
{
// There are two cases when this method is called:
// 1. (prev == nullptr) adding a new room to the room list
// 2. (prev != nullptr) accepting/rejecting an invitation or inviting to
// the previously left room (in both cases prev has the previous state).
if (prev == room) {
qCritical() << "RoomListModel::updateRoom: room tried to replace itself";
refresh(static_cast<NeoChatRoom *>(room));
return;
}
if (prev && room->id() != prev->id()) {
qCritical() << "RoomListModel::updateRoom: attempt to update room" << room->id() << "to" << prev->id();
// That doesn't look right but technically we still can do it.
}
// Ok, we're through with pre-checks, now for the real thing.
auto newRoom = static_cast<NeoChatRoom *>(room);
const auto it = std::find_if(m_rooms.begin(), m_rooms.end(), [prev, newRoom](const NeoChatRoom *r) {
return r == prev || r == newRoom;
});
if (it != m_rooms.end()) {
const int row = it - m_rooms.begin();
// There's no guarantee that prev != newRoom
if (*it == prev && *it != newRoom) {
prev->disconnect(this);
m_rooms.replace(row, newRoom);
connectRoomSignals(newRoom);
}
Q_EMIT dataChanged(index(row), index(row));
} else {
beginInsertRows(QModelIndex(), m_rooms.count(), m_rooms.count());
doAddRoom(newRoom);
endInsertRows();
}
}
void RoomListModel::deleteRoom(Room *room)
{
qDebug() << "Deleting room" << room->id();
const auto it = std::find(m_rooms.begin(), m_rooms.end(), room);
if (it == m_rooms.end()) {
return; // Already deleted, nothing to do
}
qDebug() << "Erasing room" << room->id();
const int row = it - m_rooms.begin();
beginRemoveRows(QModelIndex(), row, row);
m_rooms.erase(it);
endRemoveRows();
}
int RoomListModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return m_rooms.count();
}
QVariant RoomListModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return QVariant();
}
if (index.row() >= m_rooms.count()) {
qDebug() << "UserListModel: something wrong here...";
return QVariant();
}
NeoChatRoom *room = m_rooms.at(index.row());
if (role == DisplayNameRole) {
return room->displayName();
}
if (role == EscapedDisplayNameRole) {
return room->displayName().toHtmlEscaped();
}
if (role == AvatarRole) {
return room->avatarMediaUrl();
}
if (role == CanonicalAliasRole) {
return room->canonicalAlias();
}
if (role == TopicRole) {
return room->topic();
}
if (role == CategoryRole) {
return NeoChatRoomType::typeForRoom(room);
}
if (role == ContextNotificationCountRole) {
return room->contextAwareNotificationCount();
}
if (role == HasHighlightNotificationsRole) {
return room->highlightCount() > 0 && room->contextAwareNotificationCount() > 0;
}
if (role == JoinStateRole) {
if (!room->successorId().isEmpty()) {
return u"upgraded"_s;
}
return QVariant::fromValue(room->joinState());
}
if (role == CurrentRoomRole) {
return QVariant::fromValue(room);
}
if (role == SubtitleTextRole) {
const auto lastEvent = room->lastEvent(m_hiddenFilter);
if (lastEvent == nullptr || room->lastEventIsSpoiler()) {
return QString();
}
return EventHandler::subtitleText(room, lastEvent);
}
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();
}
return QVariant();
}
void RoomListModel::refresh(NeoChatRoom *room, const QList<int> &roles)
{
const auto it = std::find(m_rooms.begin(), m_rooms.end(), room);
if (it == m_rooms.end()) {
qCritical() << "Room" << room->id() << "not found in the room list";
return;
}
const auto idx = index(it - m_rooms.begin());
Q_EMIT dataChanged(idx, idx, roles);
}
QHash<int, QByteArray> RoomListModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[DisplayNameRole] = "displayName";
roles[EscapedDisplayNameRole] = "escapedDisplayName";
roles[AvatarRole] = "avatar";
roles[CanonicalAliasRole] = "canonicalAlias";
roles[TopicRole] = "topic";
roles[CategoryRole] = "category";
roles[ContextNotificationCountRole] = "contextNotificationCount";
roles[HasHighlightNotificationsRole] = "hasHighlightNotifications";
roles[JoinStateRole] = "joinState";
roles[CurrentRoomRole] = "currentRoom";
roles[SubtitleTextRole] = "subtitleText";
roles[IsSpaceRole] = "isSpace";
roles[RoomIdRole] = "roomId";
roles[IsChildSpaceRole] = "isChildSpace";
roles[IsDirectChat] = "isDirectChat";
return roles;
}
NeoChatRoom *RoomListModel::roomByAliasOrId(const QString &aliasOrId)
{
for (const auto &room : std::as_const(m_rooms)) {
if (room->aliases().contains(aliasOrId) || room->id() == aliasOrId) {
return room;
}
}
return nullptr;
}
int RoomListModel::rowForRoom(NeoChatRoom *room) const
{
return m_rooms.indexOf(room);
}
void RoomListModel::setHiddenFilter(std::function<bool(const Quotient::RoomEvent *)> hiddenFilter)
{
RoomListModel::m_hiddenFilter = hiddenFilter;
}
#include "moc_roomlistmodel.cpp"

View File

@@ -0,0 +1,125 @@
// SPDX-FileCopyrightText: 2018 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <QAbstractListModel>
#include <QQmlEngine>
class NeoChatRoom;
namespace Quotient
{
class Room;
class RoomEvent;
}
class NeoChatConnection;
/**
* @class RoomListModel
*
* This class defines the model for visualising the user's list of joined rooms.
*/
class RoomListModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The current connection that the model is getting its rooms from.
*/
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. */
EscapedDisplayNameRole, /**< HTML-Escaped 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. */
ContextNotificationCountRole, /**< The context aware notification count for the room. */
HasHighlightNotificationsRole, /**< Whether there are any highlight notifications. */
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. */
};
Q_ENUM(EventRoles)
explicit RoomListModel(QObject *parent = nullptr);
~RoomListModel() override;
[[nodiscard]] NeoChatConnection *connection() const;
void setConnection(NeoChatConnection *connection);
/**
* @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 Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
Q_INVOKABLE [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa EventRoles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
/**
* @brief Return the room at the given row.
*/
Q_INVOKABLE [[nodiscard]] NeoChatRoom *roomAt(int row) const;
/**
* @brief Return the model row for the given room.
*/
Q_INVOKABLE [[nodiscard]] int rowForRoom(NeoChatRoom *room) const;
/**
* @brief Return a room for the given room alias or room matrix ID.
*
* The room must be in the model.
*/
Q_INVOKABLE NeoChatRoom *roomByAliasOrId(const QString &aliasOrId);
static void setHiddenFilter(std::function<bool(const Quotient::RoomEvent *)> hiddenFilter);
private Q_SLOTS:
void doResetModel();
void doAddRoom(Quotient::Room *room);
void updateRoom(Quotient::Room *room, Quotient::Room *prev);
void deleteRoom(Quotient::Room *room);
void refresh(NeoChatRoom *room, const QList<int> &roles = {});
private:
QPointer<NeoChatConnection> m_connection;
QList<NeoChatRoom *> m_rooms;
QString m_activeSpaceId;
void connectRoomSignals(NeoChatRoom *room);
static std::function<bool(const Quotient::RoomEvent *)> m_hiddenFilter;
Q_SIGNALS:
void connectionChanged();
void roomAdded(NeoChatRoom *_t1);
};

View File

@@ -0,0 +1,232 @@
// SPDX-FileCopyrightText: 2018-2020 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
#include "userlistmodel.h"
#include <QGuiApplication>
#include <Quotient/avatar.h>
#include <Quotient/events/roompowerlevelsevent.h>
#include "enums/powerlevel.h"
#include "neochatroom.h"
using namespace Quotient;
UserListModel::UserListModel(QObject *parent)
: QAbstractListModel(parent)
, m_currentRoom(nullptr)
{
}
void UserListModel::setRoom(NeoChatRoom *room)
{
if (m_currentRoom == room) {
return;
}
if (m_currentRoom) {
// HACK: Reset the model to a null room first to make sure QML dismantles
// last room's objects before the room is actually changed
beginResetModel();
m_currentRoom->disconnect(this);
m_currentRoom->connection()->disconnect(this);
m_currentRoom = nullptr;
m_members.clear();
endResetModel();
}
m_currentRoom = room;
if (m_currentRoom) {
connect(m_currentRoom, &Room::memberJoined, this, &UserListModel::memberJoined);
connect(m_currentRoom, &Room::memberLeft, this, &UserListModel::memberLeft);
connect(m_currentRoom, &Room::memberNameUpdated, this, [this](RoomMember member) {
refreshMember(member, {DisplayNameRole});
});
connect(m_currentRoom, &Room::memberAvatarUpdated, this, [this](RoomMember member) {
refreshMember(member, {AvatarRole});
});
connect(m_currentRoom, &Room::memberListChanged, this, [this]() {
// this is slow
UserListModel::refreshAllMembers();
});
connect(m_currentRoom->connection(), &Connection::loggedOut, this, [this]() {
setRoom(nullptr);
});
}
m_active = false;
Q_EMIT roomChanged();
}
NeoChatRoom *UserListModel::room() const
{
return m_currentRoom;
}
QVariant UserListModel::data(const QModelIndex &index, int role) const
{
if (!m_currentRoom) {
return {};
}
if (!index.isValid()) {
return QVariant();
}
if (index.row() >= m_members.count()) {
qDebug() << "UserListModel, something's wrong: index.row() >= "
"users.count()";
return {};
}
auto memberId = m_members.at(index.row());
if (role == DisplayNameRole) {
return m_currentRoom->member(memberId).disambiguatedName();
}
if (role == UserIdRole) {
return memberId;
}
if (role == AvatarRole) {
return m_currentRoom->member(memberId).avatarUrl();
}
if (role == ObjectRole) {
return QVariant::fromValue(memberId);
}
if (role == PowerLevelRole) {
auto plEvent = m_currentRoom->currentState().get<RoomPowerLevelsEvent>();
if (!plEvent) {
return 0;
}
return plEvent->powerLevelForUser(memberId);
}
if (role == PowerLevelStringRole) {
auto pl = m_currentRoom->currentState().get<RoomPowerLevelsEvent>();
// User might not in the room yet, in this case pl can be nullptr.
// e.g. When invited but user not accepted or denied the invitation.
if (!pl) {
return u"Not Available"_s;
}
auto userPl = pl->powerLevelForUser(memberId);
return i18nc("%1 is the name of the power level, e.g. admin and %2 is the value that represents.",
"%1 (%2)",
PowerLevel::nameForLevel(PowerLevel::levelForValue(userPl)),
userPl);
}
return {};
}
int UserListModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return m_members.count();
}
bool UserListModel::event(QEvent *event)
{
if (event->type() == QEvent::ApplicationPaletteChange) {
refreshAllMembers();
}
return QObject::event(event);
}
void UserListModel::memberJoined(const Quotient::RoomMember &member)
{
auto pos = findUserPos(member);
beginInsertRows(QModelIndex(), pos, pos);
m_members.insert(pos, member.id());
endInsertRows();
}
void UserListModel::memberLeft(const Quotient::RoomMember &member)
{
auto pos = findUserPos(member);
if (pos != m_members.size()) {
beginRemoveRows(QModelIndex(), pos, pos);
m_members.removeAt(pos);
endRemoveRows();
} else {
qWarning() << "Trying to remove a room member not in the user list";
}
}
void UserListModel::refreshMember(const Quotient::RoomMember &member, const QList<int> &roles)
{
auto pos = findUserPos(member);
if (pos != m_members.size()) {
// The update will have changed the state event so we need to insert the updated member object.
m_members.insert(pos, member.id());
Q_EMIT dataChanged(index(pos), index(pos), roles);
} else {
qWarning() << "Trying to access a room member not in the user list";
}
}
void UserListModel::refreshAllMembers()
{
beginResetModel();
if (m_currentRoom != nullptr) {
m_members = m_currentRoom->joinedMemberIds();
MemberSorter sorter;
std::sort(m_members.begin(), m_members.end(), [&sorter, this](const auto &left, const auto &right) {
const auto leftPl = m_currentRoom->memberEffectivePowerLevel(left);
const auto rightPl = m_currentRoom->memberEffectivePowerLevel(right);
if (leftPl > rightPl) {
return true;
} else if (rightPl > leftPl) {
return false;
}
return sorter(m_currentRoom->member(left), m_currentRoom->member(right));
});
}
endResetModel();
Q_EMIT usersRefreshed();
}
int UserListModel::findUserPos(const RoomMember &member) const
{
return findUserPos(member.id());
}
int UserListModel::findUserPos(const QString &userId) const
{
if (!m_currentRoom) {
return 0;
}
const auto pos = std::find_if(m_members.cbegin(), m_members.cend(), [&userId](const QString &memberId) {
return userId == memberId;
});
return pos - m_members.cbegin();
}
QHash<int, QByteArray> UserListModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[DisplayNameRole] = "name";
roles[UserIdRole] = "userId";
roles[AvatarRole] = "avatar";
roles[ObjectRole] = "user";
roles[PowerLevelRole] = "powerLevel";
roles[PowerLevelStringRole] = "powerLevelString";
return roles;
}
void UserListModel::activate()
{
if (m_active) {
return;
}
m_active = true;
refreshAllMembers();
}
#include "moc_userlistmodel.cpp"

View File

@@ -0,0 +1,103 @@
// SPDX-FileCopyrightText: 2018 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <Quotient/room.h>
#include <QAbstractListModel>
#include <QObject>
#include <QPointer>
#include <QQmlEngine>
class NeoChatRoom;
namespace Quotient
{
class User;
}
/**
* @class UserListModel
*
* This class defines the model for listing the users in a room.
*
* As well as gathering all the users from a room, the model ensures that they are
* sorted in alphabetical order.
*
* @sa NeoChatRoom
*/
class UserListModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The room that the model is getting its users from.
*/
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
public:
/**
* @brief Defines the model roles.
*/
enum EventRoles {
DisplayNameRole = Qt::DisplayRole, /**< The user's display name in the current room. */
UserIdRole, /**< Matrix ID of the user. */
AvatarRole, /**< The source URL for the user's avatar in the current room. */
ObjectRole, /**< The QObject for the user. */
PowerLevelRole, /**< The user's power level in the current room. */
PowerLevelStringRole, /**< The name of the user's power level in the current room. */
};
Q_ENUM(EventRoles)
explicit UserListModel(QObject *parent = nullptr);
[[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa EventRoles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
void activate();
Q_SIGNALS:
void roomChanged();
void usersRefreshed();
protected:
bool event(QEvent *event) override;
private Q_SLOTS:
void memberJoined(const Quotient::RoomMember &member);
void memberLeft(const Quotient::RoomMember &member);
void refreshMember(const Quotient::RoomMember &member, const QList<int> &roles = {});
void refreshAllMembers();
private:
QPointer<NeoChatRoom> m_currentRoom;
QList<QString> m_members;
bool m_active = false;
int findUserPos(const Quotient::RoomMember &member) const;
[[nodiscard]] int findUserPos(const QString &username) const;
};