Move ChatDocumentHandler and related includes to LibNeoChat
This commit is contained in:
@@ -9,6 +9,7 @@ target_sources(LibNeoChat PRIVATE
|
||||
neochatroommember.cpp
|
||||
accountmanager.cpp
|
||||
chatbarcache.cpp
|
||||
chatdocumenthandler.cpp
|
||||
clipboard.cpp
|
||||
delegatesizehelper.cpp
|
||||
emojitones.cpp
|
||||
@@ -28,12 +29,16 @@ target_sources(LibNeoChat PRIVATE
|
||||
events/imagepackevent.cpp
|
||||
events/pollevent.cpp
|
||||
models/actionsmodel.cpp
|
||||
models/completionmodel.cpp
|
||||
models/completionproxymodel.cpp
|
||||
models/customemojimodel.cpp
|
||||
models/emojimodel.cpp
|
||||
models/imagepacksmodel.cpp
|
||||
models/livelocationsmodel.cpp
|
||||
models/locationsmodel.cpp
|
||||
models/roomlistmodel.cpp
|
||||
models/stickermodel.cpp
|
||||
models/userlistmodel.cpp
|
||||
)
|
||||
|
||||
ecm_add_qml_module(LibNeoChat GENERATE_PLUGIN_SOURCE
|
||||
@@ -48,16 +53,25 @@ ecm_qt_declare_logging_category(LibNeoChat
|
||||
DEFAULT_SEVERITY Info
|
||||
)
|
||||
|
||||
ecm_qt_declare_logging_category(LibNeoChat
|
||||
HEADER "chatdocumenthandler_logging.h"
|
||||
IDENTIFIER "ChatDocumentHandling"
|
||||
CATEGORY_NAME "org.kde.neochat.chatdocumenthandler"
|
||||
DEFAULT_SEVERITY Info
|
||||
)
|
||||
|
||||
generate_export_header(LibNeoChat BASE_NAME LibNeoChat)
|
||||
target_include_directories(LibNeoChat PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/enums ${CMAKE_CURRENT_SOURCE_DIR}/events ${CMAKE_CURRENT_SOURCE_DIR}/models)
|
||||
target_link_libraries(LibNeoChat PUBLIC
|
||||
Qt::Core
|
||||
Qt::Multimedia
|
||||
Qt::Quick
|
||||
Qt::QuickControls2
|
||||
KF6::ConfigCore
|
||||
KF6::CoreAddons
|
||||
KF6::I18n
|
||||
KF6::Kirigami
|
||||
KF6::SonnetCore
|
||||
QuotientQt6
|
||||
cmark::cmark
|
||||
QCoro::Core
|
||||
|
||||
399
src/libneochat/chatdocumenthandler.cpp
Normal file
399
src/libneochat/chatdocumenthandler.cpp
Normal file
@@ -0,0 +1,399 @@
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "chatdocumenthandler.h"
|
||||
|
||||
#include <QQmlFile>
|
||||
#include <QQmlFileSelector>
|
||||
#include <QStringBuilder>
|
||||
#include <QSyntaxHighlighter>
|
||||
#include <QTextBlock>
|
||||
#include <QTextDocument>
|
||||
#include <QTimer>
|
||||
|
||||
#include <Sonnet/BackgroundChecker>
|
||||
#include <Sonnet/Settings>
|
||||
|
||||
#include "chatdocumenthandler_logging.h"
|
||||
#include "eventhandler.h"
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
class SyntaxHighlighter : public QSyntaxHighlighter
|
||||
{
|
||||
public:
|
||||
QTextCharFormat mentionFormat;
|
||||
QTextCharFormat errorFormat;
|
||||
Sonnet::BackgroundChecker *checker = new Sonnet::BackgroundChecker;
|
||||
Sonnet::Settings settings;
|
||||
QList<QPair<int, QString>> errors;
|
||||
QString previousText;
|
||||
QTimer rehighlightTimer;
|
||||
SyntaxHighlighter(QObject *parent)
|
||||
: QSyntaxHighlighter(parent)
|
||||
{
|
||||
mentionFormat.setFontWeight(QFont::Bold);
|
||||
mentionFormat.setForeground(Qt::blue);
|
||||
|
||||
errorFormat.setForeground(Qt::red);
|
||||
errorFormat.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
|
||||
|
||||
connect(checker, &Sonnet::BackgroundChecker::misspelling, this, [this](const QString &word, int start) {
|
||||
errors += {start, word};
|
||||
checker->continueChecking();
|
||||
});
|
||||
connect(checker, &Sonnet::BackgroundChecker::done, this, [this]() {
|
||||
rehighlightTimer.start();
|
||||
});
|
||||
rehighlightTimer.setInterval(100);
|
||||
rehighlightTimer.setSingleShot(true);
|
||||
rehighlightTimer.callOnTimeout(this, &QSyntaxHighlighter::rehighlight);
|
||||
}
|
||||
void highlightBlock(const QString &text) override
|
||||
{
|
||||
if (settings.checkerEnabledByDefault()) {
|
||||
if (text != previousText) {
|
||||
previousText = text;
|
||||
checker->stop();
|
||||
errors.clear();
|
||||
checker->setText(text);
|
||||
}
|
||||
for (const auto &error : errors) {
|
||||
setFormat(error.first, error.second.size(), errorFormat);
|
||||
}
|
||||
}
|
||||
auto handler = dynamic_cast<ChatDocumentHandler *>(parent());
|
||||
auto room = handler->room();
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
auto mentions = handler->chatBarCache()->mentions();
|
||||
mentions->erase(std::remove_if(mentions->begin(),
|
||||
mentions->end(),
|
||||
[this](auto &mention) {
|
||||
if (document()->toPlainText().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mention.cursor.position() == 0 && mention.cursor.anchor() == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mention.cursor.position() - mention.cursor.anchor() != mention.text.size()) {
|
||||
mention.cursor.setPosition(mention.start);
|
||||
mention.cursor.setPosition(mention.cursor.anchor() + mention.text.size(), QTextCursor::KeepAnchor);
|
||||
}
|
||||
|
||||
if (mention.cursor.selectedText() != mention.text) {
|
||||
return true;
|
||||
}
|
||||
if (currentBlock() == mention.cursor.block()) {
|
||||
mention.start = mention.cursor.anchor();
|
||||
mention.position = mention.cursor.position();
|
||||
setFormat(mention.cursor.selectionStart(), mention.cursor.selectedText().size(), mentionFormat);
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
mentions->end());
|
||||
}
|
||||
};
|
||||
|
||||
ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_document(nullptr)
|
||||
, m_cursorPosition(-1)
|
||||
, m_highlighter(new SyntaxHighlighter(this))
|
||||
, m_completionModel(new CompletionModel(this))
|
||||
{
|
||||
connect(this, &ChatDocumentHandler::roomChanged, this, [this]() {
|
||||
m_completionModel->setRoom(m_room);
|
||||
static QPointer<NeoChatRoom> previousRoom = nullptr;
|
||||
if (previousRoom) {
|
||||
disconnect(m_chatBarCache, &ChatBarCache::textChanged, this, nullptr);
|
||||
}
|
||||
previousRoom = m_room;
|
||||
connect(m_chatBarCache, &ChatBarCache::textChanged, this, [this]() {
|
||||
int start = completionStartIndex();
|
||||
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
|
||||
});
|
||||
});
|
||||
connect(this, &ChatDocumentHandler::documentChanged, this, [this]() {
|
||||
m_highlighter->setDocument(m_document->textDocument());
|
||||
});
|
||||
connect(this, &ChatDocumentHandler::cursorPositionChanged, this, [this]() {
|
||||
if (!m_room) {
|
||||
return;
|
||||
}
|
||||
int start = completionStartIndex();
|
||||
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
|
||||
});
|
||||
}
|
||||
|
||||
int ChatDocumentHandler::completionStartIndex() const
|
||||
{
|
||||
if (!m_room) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const qsizetype cursor = cursorPosition();
|
||||
const auto &text = getText();
|
||||
|
||||
auto start = std::min(cursor, text.size()) - 1;
|
||||
while (start > -1) {
|
||||
if (text.at(start) == QLatin1Char(' ')) {
|
||||
start++;
|
||||
break;
|
||||
}
|
||||
start--;
|
||||
}
|
||||
return start;
|
||||
}
|
||||
|
||||
QQuickTextDocument *ChatDocumentHandler::document() const
|
||||
{
|
||||
return m_document;
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::setDocument(QQuickTextDocument *document)
|
||||
{
|
||||
if (document == m_document) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_document) {
|
||||
m_document->textDocument()->disconnect(this);
|
||||
}
|
||||
m_document = document;
|
||||
Q_EMIT documentChanged();
|
||||
}
|
||||
|
||||
int ChatDocumentHandler::cursorPosition() const
|
||||
{
|
||||
return m_cursorPosition;
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::setCursorPosition(int position)
|
||||
{
|
||||
if (position == m_cursorPosition) {
|
||||
return;
|
||||
}
|
||||
if (m_room) {
|
||||
m_cursorPosition = position;
|
||||
}
|
||||
Q_EMIT cursorPositionChanged();
|
||||
}
|
||||
|
||||
NeoChatRoom *ChatDocumentHandler::room() const
|
||||
{
|
||||
return m_room;
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::setRoom(NeoChatRoom *room)
|
||||
{
|
||||
if (m_room == room) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_room = room;
|
||||
Q_EMIT roomChanged();
|
||||
}
|
||||
|
||||
ChatBarCache *ChatDocumentHandler::chatBarCache() const
|
||||
{
|
||||
return m_chatBarCache;
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::setChatBarCache(ChatBarCache *chatBarCache)
|
||||
{
|
||||
if (m_chatBarCache == chatBarCache) {
|
||||
return;
|
||||
}
|
||||
m_chatBarCache = chatBarCache;
|
||||
Q_EMIT chatBarCacheChanged();
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::complete(int index)
|
||||
{
|
||||
if (m_document == nullptr) {
|
||||
qCWarning(ChatDocumentHandling) << "complete called with m_document set to nullptr.";
|
||||
return;
|
||||
}
|
||||
if (m_completionModel->autoCompletionType() == CompletionModel::None) {
|
||||
qCWarning(ChatDocumentHandling) << "complete called with m_completionModel->autoCompletionType() == CompletionModel::None.";
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure we only search for the beginning of the current completion identifier
|
||||
const auto fromIndex = qMax(completionStartIndex(), 0);
|
||||
|
||||
if (m_completionModel->autoCompletionType() == CompletionModel::User) {
|
||||
auto name = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::DisplayNameRole).toString();
|
||||
auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString();
|
||||
auto text = getText();
|
||||
auto at = text.indexOf(QLatin1Char('@'), fromIndex);
|
||||
QTextCursor cursor(document()->textDocument());
|
||||
cursor.setPosition(at);
|
||||
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
|
||||
cursor.insertText(name + u" "_s);
|
||||
cursor.setPosition(at);
|
||||
cursor.setPosition(cursor.position() + name.size(), QTextCursor::KeepAnchor);
|
||||
cursor.setKeepPositionOnInsert(true);
|
||||
pushMention({cursor, name, 0, 0, id});
|
||||
m_highlighter->rehighlight();
|
||||
} else if (m_completionModel->autoCompletionType() == CompletionModel::Command) {
|
||||
auto command = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedTextRole).toString();
|
||||
auto text = getText();
|
||||
auto at = text.indexOf(QLatin1Char('/'), fromIndex);
|
||||
QTextCursor cursor(document()->textDocument());
|
||||
cursor.setPosition(at);
|
||||
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
|
||||
cursor.insertText(u"/%1 "_s.arg(command));
|
||||
} else if (m_completionModel->autoCompletionType() == CompletionModel::Room) {
|
||||
auto alias = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString();
|
||||
auto text = getText();
|
||||
auto at = text.indexOf(QLatin1Char('#'), fromIndex);
|
||||
QTextCursor cursor(document()->textDocument());
|
||||
cursor.setPosition(at);
|
||||
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
|
||||
cursor.insertText(alias + u" "_s);
|
||||
cursor.setPosition(at);
|
||||
cursor.setPosition(cursor.position() + alias.size(), QTextCursor::KeepAnchor);
|
||||
cursor.setKeepPositionOnInsert(true);
|
||||
pushMention({cursor, alias, 0, 0, alias});
|
||||
m_highlighter->rehighlight();
|
||||
} else if (m_completionModel->autoCompletionType() == CompletionModel::Emoji) {
|
||||
auto shortcode = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedTextRole).toString();
|
||||
auto text = getText();
|
||||
auto at = text.indexOf(QLatin1Char(':'), fromIndex);
|
||||
QTextCursor cursor(document()->textDocument());
|
||||
cursor.setPosition(at);
|
||||
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
|
||||
cursor.insertText(shortcode);
|
||||
}
|
||||
}
|
||||
|
||||
CompletionModel *ChatDocumentHandler::completionModel() const
|
||||
{
|
||||
return m_completionModel;
|
||||
}
|
||||
|
||||
int ChatDocumentHandler::selectionStart() const
|
||||
{
|
||||
return m_selectionStart;
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::setSelectionStart(int position)
|
||||
{
|
||||
if (position == m_selectionStart) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_selectionStart = position;
|
||||
Q_EMIT selectionStartChanged();
|
||||
}
|
||||
|
||||
int ChatDocumentHandler::selectionEnd() const
|
||||
{
|
||||
return m_selectionEnd;
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::setSelectionEnd(int position)
|
||||
{
|
||||
if (position == m_selectionEnd) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_selectionEnd = position;
|
||||
Q_EMIT selectionEndChanged();
|
||||
}
|
||||
|
||||
QString ChatDocumentHandler::getText() const
|
||||
{
|
||||
if (!m_chatBarCache) {
|
||||
qCWarning(ChatDocumentHandling) << "getText called with m_chatBarCache set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
return m_chatBarCache->text();
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::pushMention(const Mention mention) const
|
||||
{
|
||||
if (!m_chatBarCache) {
|
||||
qCWarning(ChatDocumentHandling) << "pushMention called with m_chatBarCache set to nullptr.";
|
||||
return;
|
||||
}
|
||||
m_chatBarCache->mentions()->push_back(mention);
|
||||
}
|
||||
|
||||
QColor ChatDocumentHandler::mentionColor() const
|
||||
{
|
||||
return m_mentionColor;
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::setMentionColor(const QColor &color)
|
||||
{
|
||||
if (m_mentionColor == color) {
|
||||
return;
|
||||
}
|
||||
m_mentionColor = color;
|
||||
m_highlighter->mentionFormat.setForeground(m_mentionColor);
|
||||
m_highlighter->rehighlight();
|
||||
Q_EMIT mentionColorChanged();
|
||||
}
|
||||
|
||||
QColor ChatDocumentHandler::errorColor() const
|
||||
{
|
||||
return m_errorColor;
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::setErrorColor(const QColor &color)
|
||||
{
|
||||
if (m_errorColor == color) {
|
||||
return;
|
||||
}
|
||||
m_errorColor = color;
|
||||
m_highlighter->errorFormat.setForeground(m_errorColor);
|
||||
m_highlighter->rehighlight();
|
||||
Q_EMIT errorColorChanged();
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::updateMentions(QQuickTextDocument *document, const QString &editId)
|
||||
{
|
||||
setDocument(document);
|
||||
|
||||
if (editId.isEmpty() || !m_chatBarCache || !m_room) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto event = m_room->findInTimeline(editId); event != m_room->historyEdge()) {
|
||||
if (const auto &roomMessageEvent = &*event->viewAs<Quotient::RoomMessageEvent>()) {
|
||||
// Replaces the mentions that are baked into the HTML but plaintext in the original markdown
|
||||
const QRegularExpression re(uR"lit(<a\shref="https:\/\/matrix.to\/#\/([\S]*)"\s?>([\S]*)<\/a>)lit"_s);
|
||||
|
||||
m_chatBarCache->mentions()->clear();
|
||||
|
||||
int linkSize = 0;
|
||||
auto matches = re.globalMatch(EventHandler::rawMessageBody(*roomMessageEvent));
|
||||
while (matches.hasNext()) {
|
||||
const QRegularExpressionMatch match = matches.next();
|
||||
if (match.hasMatch()) {
|
||||
const QString id = match.captured(1);
|
||||
const QString name = match.captured(2);
|
||||
|
||||
const int position = match.capturedStart(0) - linkSize;
|
||||
const int end = position + name.length();
|
||||
linkSize += match.capturedLength(0) - name.length();
|
||||
|
||||
QTextCursor cursor(this->document()->textDocument());
|
||||
cursor.setPosition(position);
|
||||
cursor.setPosition(end, QTextCursor::KeepAnchor);
|
||||
cursor.setKeepPositionOnInsert(true);
|
||||
|
||||
pushMention(Mention{.cursor = cursor, .text = name, .start = position, .position = end, .id = id});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#include "moc_chatdocumenthandler.cpp"
|
||||
180
src/libneochat/chatdocumenthandler.h
Normal file
180
src/libneochat/chatdocumenthandler.h
Normal file
@@ -0,0 +1,180 @@
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
#include <QQuickTextDocument>
|
||||
#include <QTextCursor>
|
||||
|
||||
#include "chatbarcache.h"
|
||||
#include "models/completionmodel.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
class NeoChatRoom;
|
||||
class SyntaxHighlighter;
|
||||
|
||||
/**
|
||||
* @class ChatDocumentHandler
|
||||
*
|
||||
* Handle the QQuickTextDocument of a qml text item.
|
||||
*
|
||||
* The class provides functionality to highlight text in the text document as well
|
||||
* as providing completion functionality via a CompletionModel.
|
||||
*
|
||||
* The ChatDocumentHandler is also linked to a NeoChatRoom to provide functionality
|
||||
* to save the chat document text when switching between rooms.
|
||||
*
|
||||
* To get the full functionality the cursor position and text selection information
|
||||
* need to be passed in. For example:
|
||||
*
|
||||
* @code{.qml}
|
||||
* import QtQuick 2.0
|
||||
* import QtQuick.Controls 2.15 as QQC2
|
||||
*
|
||||
* import org.kde.kirigami 2.12 as Kirigami
|
||||
* import org.kde.neochat 1.0
|
||||
*
|
||||
* QQC2.TextArea {
|
||||
* id: textField
|
||||
*
|
||||
* // Set this to a NeoChatRoom object.
|
||||
* property var room
|
||||
*
|
||||
* ChatDocumentHandler {
|
||||
* id: documentHandler
|
||||
* document: textField.textDocument
|
||||
* cursorPosition: textField.cursorPosition
|
||||
* selectionStart: textField.selectionStart
|
||||
* selectionEnd: textField.selectionEnd
|
||||
* mentionColor: Kirigami.Theme.linkColor
|
||||
* errorColor: Kirigami.Theme.negativeTextColor
|
||||
* room: textField.room
|
||||
* }
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* @sa QQuickTextDocument, CompletionModel, NeoChatRoom
|
||||
*/
|
||||
class ChatDocumentHandler : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
/**
|
||||
* @brief The QQuickTextDocument that is being handled.
|
||||
*/
|
||||
Q_PROPERTY(QQuickTextDocument *document READ document WRITE setDocument NOTIFY documentChanged)
|
||||
|
||||
/**
|
||||
* @brief The current saved cursor position.
|
||||
*/
|
||||
Q_PROPERTY(int cursorPosition READ cursorPosition WRITE setCursorPosition NOTIFY cursorPositionChanged)
|
||||
|
||||
/**
|
||||
* @brief The start position of any currently selected text.
|
||||
*/
|
||||
Q_PROPERTY(int selectionStart READ selectionStart WRITE setSelectionStart NOTIFY selectionStartChanged)
|
||||
|
||||
/**
|
||||
* @brief The end position of any currently selected text.
|
||||
*/
|
||||
Q_PROPERTY(int selectionEnd READ selectionEnd WRITE setSelectionEnd NOTIFY selectionEndChanged)
|
||||
|
||||
/**
|
||||
* @brief The current CompletionModel.
|
||||
*
|
||||
* This is typically provided to a qml component to visualise the current
|
||||
* completion results.
|
||||
*/
|
||||
Q_PROPERTY(CompletionModel *completionModel READ completionModel CONSTANT)
|
||||
|
||||
/**
|
||||
* @brief The current room that the text document is being handled for.
|
||||
*/
|
||||
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||
|
||||
/**
|
||||
* @brief The cache for the chat bar the text document is being handled for.
|
||||
*/
|
||||
Q_PROPERTY(ChatBarCache *chatBarCache READ chatBarCache WRITE setChatBarCache NOTIFY chatBarCacheChanged)
|
||||
|
||||
/**
|
||||
* @brief The color to highlight user mentions.
|
||||
*/
|
||||
Q_PROPERTY(QColor mentionColor READ mentionColor WRITE setMentionColor NOTIFY mentionColorChanged)
|
||||
|
||||
/**
|
||||
* @brief The color to highlight spelling errors.
|
||||
*/
|
||||
Q_PROPERTY(QColor errorColor READ errorColor WRITE setErrorColor NOTIFY errorColorChanged)
|
||||
|
||||
public:
|
||||
explicit ChatDocumentHandler(QObject *parent = nullptr);
|
||||
|
||||
[[nodiscard]] QQuickTextDocument *document() const;
|
||||
void setDocument(QQuickTextDocument *document);
|
||||
|
||||
[[nodiscard]] int cursorPosition() const;
|
||||
void setCursorPosition(int position);
|
||||
|
||||
[[nodiscard]] int selectionStart() const;
|
||||
void setSelectionStart(int position);
|
||||
|
||||
[[nodiscard]] int selectionEnd() const;
|
||||
void setSelectionEnd(int position);
|
||||
|
||||
[[nodiscard]] NeoChatRoom *room() const;
|
||||
void setRoom(NeoChatRoom *room);
|
||||
|
||||
[[nodiscard]] ChatBarCache *chatBarCache() const;
|
||||
void setChatBarCache(ChatBarCache *chatBarCache);
|
||||
|
||||
Q_INVOKABLE void complete(int index);
|
||||
|
||||
CompletionModel *completionModel() const;
|
||||
|
||||
[[nodiscard]] QColor mentionColor() const;
|
||||
void setMentionColor(const QColor &color);
|
||||
|
||||
[[nodiscard]] QColor errorColor() const;
|
||||
void setErrorColor(const QColor &color);
|
||||
|
||||
/**
|
||||
* @brief Update the mentions in @p document when editing a message.
|
||||
*/
|
||||
Q_INVOKABLE void updateMentions(QQuickTextDocument *document, const QString &editId);
|
||||
|
||||
Q_SIGNALS:
|
||||
void documentChanged();
|
||||
void cursorPositionChanged();
|
||||
void roomChanged();
|
||||
void chatBarCacheChanged();
|
||||
void selectionStartChanged();
|
||||
void selectionEndChanged();
|
||||
void errorColorChanged();
|
||||
void mentionColorChanged();
|
||||
|
||||
private:
|
||||
int completionStartIndex() const;
|
||||
|
||||
QPointer<QQuickTextDocument> m_document;
|
||||
|
||||
QPointer<NeoChatRoom> m_room;
|
||||
QPointer<ChatBarCache> m_chatBarCache;
|
||||
|
||||
QColor m_mentionColor;
|
||||
QColor m_errorColor;
|
||||
|
||||
int m_cursorPosition;
|
||||
int m_selectionStart;
|
||||
int m_selectionEnd;
|
||||
|
||||
QString getText() const;
|
||||
void pushMention(const Mention mention) const;
|
||||
|
||||
SyntaxHighlighter *m_highlighter = nullptr;
|
||||
|
||||
CompletionModel *m_completionModel = nullptr;
|
||||
};
|
||||
95
src/libneochat/enums/neochatroomtype.h
Normal file
95
src/libneochat/enums/neochatroomtype.h
Normal file
@@ -0,0 +1,95 @@
|
||||
// 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>
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
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, /**< 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. */
|
||||
TypesCount, /**< Number of different types (this should always be last). */
|
||||
};
|
||||
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");
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
static QString typeIconName(int category)
|
||||
{
|
||||
switch (category) {
|
||||
case NeoChatRoomType::Invited:
|
||||
return u"user-invisible"_s;
|
||||
case NeoChatRoomType::Favorite:
|
||||
return u"favorite"_s;
|
||||
case NeoChatRoomType::Direct:
|
||||
return u"dialog-messages"_s;
|
||||
case NeoChatRoomType::Normal:
|
||||
return u"group"_s;
|
||||
case NeoChatRoomType::Deprioritized:
|
||||
return u"object-order-lower"_s;
|
||||
case NeoChatRoomType::Space:
|
||||
return u"group"_s;
|
||||
default:
|
||||
return u"tools-report-bug"_s;
|
||||
}
|
||||
}
|
||||
};
|
||||
208
src/libneochat/models/completionmodel.cpp
Normal file
208
src/libneochat/models/completionmodel.cpp
Normal 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"
|
||||
139
src/libneochat/models/completionmodel.h
Normal file
139
src/libneochat/models/completionmodel.h
Normal 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);
|
||||
64
src/libneochat/models/completionproxymodel.cpp
Normal file
64
src/libneochat/models/completionproxymodel.cpp
Normal 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"
|
||||
77
src/libneochat/models/completionproxymodel.h
Normal file
77
src/libneochat/models/completionproxymodel.h
Normal 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;
|
||||
};
|
||||
328
src/libneochat/models/roomlistmodel.cpp
Normal file
328
src/libneochat/models/roomlistmodel.cpp
Normal 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"
|
||||
125
src/libneochat/models/roomlistmodel.h
Normal file
125
src/libneochat/models/roomlistmodel.h
Normal 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);
|
||||
};
|
||||
232
src/libneochat/models/userlistmodel.cpp
Normal file
232
src/libneochat/models/userlistmodel.cpp
Normal 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"
|
||||
103
src/libneochat/models/userlistmodel.h
Normal file
103
src/libneochat/models/userlistmodel.h
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user